diff --git a/assets/admin/src/api/index.js b/assets/admin/src/api/index.js index 9f0fdc15..2dad120a 100644 --- a/assets/admin/src/api/index.js +++ b/assets/admin/src/api/index.js @@ -94,8 +94,7 @@ class ApiClient { async request(endpointOrConfig, options = {}) { // Dual-signature support: - // request('/forum/overview') - // request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } }) + // request('/users/1/restrictions') // request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' }) let endpoint, method, headers, params, body, rest diff --git a/assets/admin/src/constants/sections/forum.js b/assets/admin/src/constants/sections/forum.js deleted file mode 100644 index 6f3c07f0..00000000 --- a/assets/admin/src/constants/sections/forum.js +++ /dev/null @@ -1,111 +0,0 @@ -import { renderFaIcon, schemaPage } from './helpers' - -// Forum SPA = config only. Ops moved to native CPT menus (Phase 4). -export const forumSection = { - key: 'forum', - label: '社区&论坛', - icon: renderFaIcon('fas fa-comments'), - route: '/forum', - children: [ - schemaPage({ - key: 'forum-settings-basic', - name: 'ForumSettingsBasic', - label: '基础设置', - route: '/forum/settings', - title: '论坛基础设置', - desc: '论坛总开关、管理员和名称定义。', - canonicalDomain: 'forum', - section: 'basic', - }), - schemaPage({ - key: 'forum-settings-names', - name: 'ForumSettingsNames', - label: '名称定义', - route: '/forum/settings/names', - title: '名称定义', - desc: '自定义论坛各实体的显示名称。', - canonicalDomain: 'forum', - section: 'names', - }), - schemaPage({ - key: 'forum-settings-homepage', - name: 'ForumSettingsHomepage', - label: '首页设置', - route: '/forum/settings/homepage', - title: '论坛首页设置', - desc: '首页 Tab 引擎、快速发帖入口和移动端交互配置。', - canonicalDomain: 'forum', - section: 'homepage', - }), - schemaPage({ - key: 'forum-settings-board-page', - name: 'ForumSettingsBoardPage', - label: '版块页面', - route: '/forum/settings/board-page', - title: '版块页面设置', - desc: '版块页布局、Tab 栏目和移动端交互配置。', - canonicalDomain: 'forum', - section: 'board_page', - }), - schemaPage({ - key: 'forum-settings-post-page', - name: 'ForumSettingsPostPage', - label: '帖子页面', - route: '/forum/settings/post-page', - title: '帖子页面设置', - desc: '面包屑、评分、高度限制和封面配置。', - canonicalDomain: 'forum', - section: 'post_page', - }), - schemaPage({ - key: 'forum-settings-reply', - name: 'ForumSettingsReply', - label: '回复评论', - route: '/forum/settings/reply', - title: '回复评论设置', - desc: '回复翻页方式、工具栏功能和快捷回复配置。', - canonicalDomain: 'forum', - section: 'reply', - }), - schemaPage({ - key: 'forum-settings-permissions', - name: 'ForumSettingsPermissions', - label: '权限配置', - route: '/forum/settings/permissions', - title: '论坛权限配置', - desc: '发帖限制、帖子付费和版块付费关注。', - canonicalDomain: 'forum', - section: 'permissions', - }), - schemaPage({ - key: 'forum-settings-other', - name: 'ForumSettingsOther', - label: '其它设置', - route: '/forum/settings/other', - title: '论坛其它设置', - desc: '投票、版主申请说明、标题字数和 URL 别名。', - canonicalDomain: 'forum', - section: 'other', - }), - schemaPage({ - key: 'forum-settings-upload', - name: 'ForumSettingsUpload', - label: '上传权限', - route: '/forum/settings/upload', - title: '上传权限设置', - desc: '图片、视频和文件上传大小与格式限制。', - canonicalDomain: 'forum', - section: 'upload', - }), - schemaPage({ - key: 'forum-settings-credits', - name: 'ForumSettingsCredits', - label: '积分设置', - route: '/forum/settings/credits', - title: '论坛积分设置', - desc: '论坛行为积分奖励矩阵和每日上限。', - canonicalDomain: 'forum', - section: 'credits', - }), - ], -} diff --git a/assets/admin/src/constants/sections/index.js b/assets/admin/src/constants/sections/index.js index bf19d631..6a0cd22f 100644 --- a/assets/admin/src/constants/sections/index.js +++ b/assets/admin/src/constants/sections/index.js @@ -4,8 +4,6 @@ import { postListSection } from './post-list' import { permissionsSection } from './permissions' import { usersSection } from './users' import { paymentSection } from './payment' -import { shopSection } from './shop' -import { forumSection } from './forum' import { extensionsSection } from './extensions' import { themeInfoSection } from './theme-info' import { docsSection } from './docs' @@ -18,8 +16,6 @@ const SECTION_DEFAULT_CHILDREN = { permissions: 'permissions-comment', users: 'users-auth', payment: 'payment-paid', - shop: 'shop-basic', - forum: 'forum-settings-basic', extensions: 'extensions-ip', tools: 'tools-import-export', 'theme-info': 'theme-info-dashboard', @@ -47,8 +43,6 @@ export const ADMIN_SECTIONS = [ attachSectionDefaultRoute(permissionsSection), attachSectionDefaultRoute(usersSection), attachSectionDefaultRoute(paymentSection), - attachSectionDefaultRoute(shopSection), - attachSectionDefaultRoute(forumSection), attachSectionDefaultRoute(extensionsSection), attachSectionDefaultRoute(toolsSection), attachSectionDefaultRoute(themeInfoSection), diff --git a/assets/admin/src/constants/sections/shop.js b/assets/admin/src/constants/sections/shop.js deleted file mode 100644 index edc6fb74..00000000 --- a/assets/admin/src/constants/sections/shop.js +++ /dev/null @@ -1,81 +0,0 @@ -import { renderFaIcon, schemaPage } from './helpers' - -// Shop SPA = config only. Ops moved to native WP menus (Phase 4). -export const shopSection = { - key: 'shop', - label: '商城&商品', - icon: renderFaIcon('fas fa-shopping-cart'), - route: '/shop', - children: [ - schemaPage({ - key: 'shop-basic', - name: 'ShopBasic', - label: '商城基础', - route: '/shop/basic', - title: '商城基础', - desc: '商城总开关、首页链接、面包屑和 URL 别名设置。', - canonicalDomain: 'shop', - section: 'basic', - }), - schemaPage({ - key: 'shop-list', - name: 'ShopList', - label: '商品列表', - route: '/shop/list', - title: '商品列表', - desc: '商品列表排序、分页、卡片样式和销量显示配置。', - canonicalDomain: 'shop', - section: 'list', - }), - schemaPage({ - key: 'shop-single', - name: 'ShopSingle', - label: '商品详情', - route: '/shop/single', - title: '商品详情', - desc: '商品详情页布局、相关推荐和底部内容设置。', - canonicalDomain: 'shop', - section: 'single', - }), - schemaPage({ - key: 'shop-product-defaults', - name: 'ShopProductDefaults', - label: '商品默认参数', - route: '/shop/product-defaults', - title: '商品默认参数', - desc: '主题级商品参数默认值,作为参数继承链的根。', - canonicalDomain: 'shop', - section: 'product-defaults', - }), - schemaPage({ - key: 'shop-contact', - name: 'ShopContact', - label: '客服配置', - route: '/shop/contact', - title: '客服配置', - desc: '商品详情页客服按钮和联系方式配置。', - canonicalDomain: 'shop', - section: 'contact', - }), - schemaPage({ - key: 'shop-shipping-config', - name: 'ShopShippingConfig', - label: '物流配置', - route: '/shop/shipping-config', - title: '物流配置', - desc: '快递查询接口、物流公司列表和收货地址设置。', - canonicalDomain: 'shop', - section: 'shipping-config', - }), - schemaPage({ - key: 'shop-timing', - name: 'ShopTiming', - label: '时效与售后原因', - route: '/shop/timing', - title: '时效与售后原因', - desc: '确认收货、评价、退货时效和售后原因配置。', - canonicalDomain: 'shop', - section: 'timing', - }), - ], -} diff --git a/assets/admin/src/pages/content/FloatingButtonsSettings.vue b/assets/admin/src/pages/content/FloatingButtonsSettings.vue index 83b64306..787ba723 100644 --- a/assets/admin/src/pages/content/FloatingButtonsSettings.vue +++ b/assets/admin/src/pages/content/FloatingButtonsSettings.vue @@ -372,7 +372,6 @@ const typeOptions = [ { label: '主题切换', value: 'theme_mode' }, { label: '页面二维码', value: 'qrcode' }, { label: '发布入口', value: 'add' }, - { label: '购物车', value: 'shop_cart' }, { label: 'QQ 客服', value: 'service_qq' }, { label: '微信客服', value: 'service_wechat' }, { label: '会员入口', value: 'pay_vip' }, @@ -397,7 +396,7 @@ const addPresetOptions = computed(() => { }) // pay_vip always opens the site VIP modal on the front-end — no link required. -const needsLink = computed(() => ['custom', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover) +const needsLink = computed(() => ['custom', 'add'].includes(editingItem.type) && !editingItem.hover) // Custom hover HTML only; VIP uses modal, not free-form hover content. const supportsHover = computed(() => editingItem.type === 'custom') const supportsActionMode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type)) @@ -444,7 +443,6 @@ function applyTypeDefaults(type) { service_wechat: { icon: 'fab fa-weixin', icon_dark: '', icon_light: '', text: '微信', desc: '扫码添加微信', link: '', hover: '', action_mode: 'link', qr_size: 120 }, pay_vip: { icon: 'fas fa-gem', icon_dark: '', icon_light: '', text: '会员', desc: '开通/升级会员弹窗', link: '', hover: '', action_mode: 'link', qr_size: 120 }, add: { icon: 'fas fa-plus', icon_dark: '', icon_light: '', text: '发布', desc: '发布内容', link: '', hover: '', action_mode: 'link', qr_size: 120 }, - shop_cart: { icon: 'fas fa-shopping-cart', icon_dark: '', icon_light: '', text: '购物车', desc: '购物车入口', link: '', hover: '', action_mode: 'link', qr_size: 120 }, } return map[type] || map.custom } diff --git a/assets/admin/src/pages/content/FooterTabbarSettings.vue b/assets/admin/src/pages/content/FooterTabbarSettings.vue index 0595b09e..3051cf89 100644 --- a/assets/admin/src/pages/content/FooterTabbarSettings.vue +++ b/assets/admin/src/pages/content/FooterTabbarSettings.vue @@ -186,8 +186,8 @@ - - + + @@ -312,7 +312,6 @@ const typeOptions = [ { label: '搜索', value: 'search' }, { label: '消息中心', value: 'msg' }, { label: '开通会员', value: 'pay_vip' }, - { label: '购物车', value: 'shop_cart' }, ] const typeLabelMap = { @@ -325,7 +324,6 @@ const typeLabelMap = { search: '搜索', msg: '消息中心', pay_vip: '开通会员', - shop_cart: '购物车', } const typeDefaults = { @@ -338,7 +336,6 @@ const typeDefaults = { search: { icon: 'fas fa-search', text: '搜索' }, msg: { icon: 'fas fa-bell', text: '消息' }, pay_vip: { icon: 'fas fa-crown', text: '会员' }, - shop_cart: { icon: 'fas fa-shopping-cart', text: '购物车' }, } const enabledPreviewItems = computed(() => form.items.filter((item) => item.enabled)) @@ -360,7 +357,6 @@ function getItemSummary(item) { if (item.type === 'auth') return '登录用户显示我的,游客显示登录' if (item.type === 'msg') return '跳转到消息中心页面' if (item.type === 'pay_vip') return item.link || '跳转到会员开通页' - if (item.type === 'shop_cart') return item.link || '跳转到购物车页面' return item.link || '自定义链接按钮' } @@ -405,7 +401,7 @@ function applyTypeDefaults(type, force = false) { const preset = typeDefaults[type] || typeDefaults.link if (force || !editingItem.icon) editingItem.icon = preset.icon if (force || !editingItem.text) editingItem.text = preset.text - if (!['link', 'msg', 'pay_vip', 'shop_cart'].includes(type)) editingItem.link = '' + if (!['link', 'msg', 'pay_vip'].includes(type)) editingItem.link = '' if (type !== 'category') editingItem.category_id = null if (type !== 'search') editingItem.search_default_type = 'post' if (type !== 'add') editingItem.add_items = [] @@ -483,7 +479,7 @@ function saveItem() { type: editingItem.type, icon: editingItem.icon, text: editingItem.text, - link: ['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type) ? editingItem.link : '', + link: ['link', 'msg', 'pay_vip'].includes(editingItem.type) ? editingItem.link : '', badge: (editingItem.badge || '').trim(), badge_mode: editingItem.badge_mode, category_id: editingItem.type === 'category' ? (editingItem.category_id || 0) : 0, diff --git a/assets/admin/src/pages/forum/ForumBoards.vue b/assets/admin/src/pages/forum/ForumBoards.vue deleted file mode 100644 index 7852b522..00000000 --- a/assets/admin/src/pages/forum/ForumBoards.vue +++ /dev/null @@ -1,195 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumDashboard.vue b/assets/admin/src/pages/forum/ForumDashboard.vue deleted file mode 100644 index a553b716..00000000 --- a/assets/admin/src/pages/forum/ForumDashboard.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumModeratorApplications.vue b/assets/admin/src/pages/forum/ForumModeratorApplications.vue deleted file mode 100644 index 4fd9adce..00000000 --- a/assets/admin/src/pages/forum/ForumModeratorApplications.vue +++ /dev/null @@ -1,201 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumModerators.vue b/assets/admin/src/pages/forum/ForumModerators.vue deleted file mode 100644 index 174b68e0..00000000 --- a/assets/admin/src/pages/forum/ForumModerators.vue +++ /dev/null @@ -1,363 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumTopics.vue b/assets/admin/src/pages/forum/ForumTopics.vue deleted file mode 100644 index 8c1f9522..00000000 --- a/assets/admin/src/pages/forum/ForumTopics.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue b/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue deleted file mode 100644 index 55a8cfb4..00000000 --- a/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue +++ /dev/null @@ -1,284 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/payment/PaymentGateway.vue b/assets/admin/src/pages/payment/PaymentGateway.vue index 0892e605..22768053 100644 --- a/assets/admin/src/pages/payment/PaymentGateway.vue +++ b/assets/admin/src/pages/payment/PaymentGateway.vue @@ -64,11 +64,11 @@ onMounted(async () => { loading.value = true try { const [gwRes, ovRes] = await Promise.all([ - api.request('/shop/gateways').catch(() => null), api.request('/payment/overview').catch(() => null), ]) - if (gwRes?.data?.gateways) { - gateways.value = gwRes.data.gateways + if (gwRes?.data?.gateway?.gateways) { + const gwList = Object.entries(gwRes.data.gateway.gateways).map(([id, enabled]) => ({ id, name: id, configured: !!enabled })) + gateways.value = gwList } if (ovRes?.data?.gateway) { overview.value = ovRes.data.gateway diff --git a/assets/admin/src/pages/shop/ShopAfterSale.vue b/assets/admin/src/pages/shop/ShopAfterSale.vue deleted file mode 100644 index 816da3f7..00000000 --- a/assets/admin/src/pages/shop/ShopAfterSale.vue +++ /dev/null @@ -1,304 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopCoupons.vue b/assets/admin/src/pages/shop/ShopCoupons.vue deleted file mode 100644 index 9b69ca6a..00000000 --- a/assets/admin/src/pages/shop/ShopCoupons.vue +++ /dev/null @@ -1,215 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopDashboard.vue b/assets/admin/src/pages/shop/ShopDashboard.vue deleted file mode 100644 index 43a48f4b..00000000 --- a/assets/admin/src/pages/shop/ShopDashboard.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopOrders.vue b/assets/admin/src/pages/shop/ShopOrders.vue deleted file mode 100644 index 7f14c2ba..00000000 --- a/assets/admin/src/pages/shop/ShopOrders.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopProducts.vue b/assets/admin/src/pages/shop/ShopProducts.vue deleted file mode 100644 index 91c8040e..00000000 --- a/assets/admin/src/pages/shop/ShopProducts.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopShipping.vue b/assets/admin/src/pages/shop/ShopShipping.vue deleted file mode 100644 index 58971363..00000000 --- a/assets/admin/src/pages/shop/ShopShipping.vue +++ /dev/null @@ -1,364 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopWithdrawals.vue b/assets/admin/src/pages/shop/ShopWithdrawals.vue deleted file mode 100644 index d831e556..00000000 --- a/assets/admin/src/pages/shop/ShopWithdrawals.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue b/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue deleted file mode 100644 index 1d3b888a..00000000 --- a/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue +++ /dev/null @@ -1,283 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/OrderContextPanel.vue b/assets/admin/src/pages/shop/components/OrderContextPanel.vue deleted file mode 100644 index b5846b1b..00000000 --- a/assets/admin/src/pages/shop/components/OrderContextPanel.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue b/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue deleted file mode 100644 index 0e0d0652..00000000 --- a/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue +++ /dev/null @@ -1,479 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue deleted file mode 100644 index e8d020cf..00000000 --- a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue +++ /dev/null @@ -1,523 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue b/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue deleted file mode 100644 index b94b8242..00000000 --- a/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue b/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue deleted file mode 100644 index 1c135f56..00000000 --- a/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue b/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue deleted file mode 100644 index ca23ce13..00000000 --- a/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/assets/admin/src/pages/tools/ToolsUpdate.vue b/assets/admin/src/pages/tools/ToolsUpdate.vue index 6057499c..fe6f396d 100644 --- a/assets/admin/src/pages/tools/ToolsUpdate.vue +++ b/assets/admin/src/pages/tools/ToolsUpdate.vue @@ -91,7 +91,6 @@ const guides = [ { key: 'meilisearch', title: 'Meilisearch', desc: '配置搜索索引与同步策略', url: '#/global/search' }, { key: 'i18n', title: '多语言翻译', desc: '检查语言切换与术语配置', url: '#/global/i18n' }, { key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' }, - { key: 'forum', title: '论坛体系', desc: '检查版块、话题与版主管理', url: '#/forum' }, { key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/security' }, { key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' }, { key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' }, diff --git a/assets/admin/src/pages/users/UsersWechatTemplate.vue b/assets/admin/src/pages/users/UsersWechatTemplate.vue index ca5e5a7b..3a157396 100644 --- a/assets/admin/src/pages/users/UsersWechatTemplate.vue +++ b/assets/admin/src/pages/users/UsersWechatTemplate.vue @@ -130,7 +130,6 @@ export default { { key: 'system_notice', desc: '系统公告通知' }, { key: 'order_paid', desc: '订单支付通知' }, { key: 'credits_change', desc: '积分变动通知' }, - { key: 'forum_reply', desc: '论坛回复通知' }, ]; const allowedTypes = ref([]); diff --git a/assets/admin/src/router/component-loaders/forum.js b/assets/admin/src/router/component-loaders/forum.js deleted file mode 100644 index d938fc9d..00000000 --- a/assets/admin/src/router/component-loaders/forum.js +++ /dev/null @@ -1,2 +0,0 @@ -// Forum ops Vue pages removed (Phase 4). Config uses SchemaPage. -export const forumPageLoaders = {} diff --git a/assets/admin/src/router/component-loaders/index.js b/assets/admin/src/router/component-loaders/index.js index d7aad84d..c38c9c28 100644 --- a/assets/admin/src/router/component-loaders/index.js +++ b/assets/admin/src/router/component-loaders/index.js @@ -2,8 +2,6 @@ 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 = { @@ -11,7 +9,5 @@ export const componentLoaders = { ...usersPageLoaders, ...paymentPageLoaders, ...toolsPageLoaders, - ...shopPageLoaders, - ...forumPageLoaders, ...extensionsPageLoaders, } diff --git a/assets/admin/src/router/component-loaders/shop.js b/assets/admin/src/router/component-loaders/shop.js deleted file mode 100644 index 1ec6c4e6..00000000 --- a/assets/admin/src/router/component-loaders/shop.js +++ /dev/null @@ -1,2 +0,0 @@ -// Shop ops Vue pages removed (Phase 4). Config uses SchemaPage. -export const shopPageLoaders = {} diff --git a/assets/admin/src/router/index.js b/assets/admin/src/router/index.js index cea75960..0d6a8833 100644 --- a/assets/admin/src/router/index.js +++ b/assets/admin/src/router/index.js @@ -73,21 +73,9 @@ function createSectionRedirectRoutes() { /** * Old SPA ops routes → native WP admin pages (Phase 4 transition). - * Bookmarks under #/shop/products etc. land on NativeOpsRedirect. + * Bookmarks under order-related paths land on NativeOpsRedirect. */ const NATIVE_OPS_REDIRECTS = [ - { path: '/shop/products', title: '商品管理', nativeAdminPath: 'edit.php?post_type=aksrt_product' }, - { path: '/shop/orders', title: '订单管理', nativeAdminPath: 'admin.php?page=aksrt_orders' }, - { path: '/shop/after-sale', title: '售后管理', nativeAdminPath: 'admin.php?page=aksrt_after_sale' }, - { path: '/shop/shipping', title: '物流管理', nativeAdminPath: 'admin.php?page=aksrt_shipping' }, - { path: '/shop/coupons', title: '优惠券', nativeAdminPath: 'admin.php?page=aksrt_coupons' }, - { path: '/shop/withdrawals', title: '提现管理', nativeAdminPath: 'admin.php?page=aksrt_withdraw' }, - { path: '/shop/dashboard', title: '商城概览', nativeAdminPath: 'admin.php?page=aksrt_pay' }, - { path: '/forum/boards', title: '板块管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/topics', title: '主题治理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' }, - { path: '/forum/moderators', title: '版主管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/moderator-applications', title: '版主申请', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/dashboard', title: '论坛概览', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' }, { path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' }, { path: '/payment/revenue', title: '创作分成', nativeAdminPath: 'admin.php?page=aksrt_income' }, { path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' }, diff --git a/assets/admin/src/schema/index.js b/assets/admin/src/schema/index.js index 64f20229..5e20efc2 100644 --- a/assets/admin/src/schema/index.js +++ b/assets/admin/src/schema/index.js @@ -34,8 +34,6 @@ 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() @@ -55,8 +53,6 @@ export const CANONICAL_SCHEMA_DOMAINS = [ 'payment', 'extensions', 'tools', - 'shop', - 'forum', ] /** All schemas organized by domain */ @@ -69,8 +65,6 @@ export const ALL_SCHEMAS = Object.freeze({ payment: paymentSchemas, extensions: extensionsSchemas, tools: toolsSchemas, - shop: shopSchemas, - forum: forumSchemas, }) /** diff --git a/assets/admin/src/schema/sections/extensions/audit.js b/assets/admin/src/schema/sections/extensions/audit.js index c0df6ec9..06e9ef7e 100644 --- a/assets/admin/src/schema/sections/extensions/audit.js +++ b/assets/admin/src/schema/sections/extensions/audit.js @@ -42,7 +42,6 @@ export const auditSchema = { 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 } }, ], }, { diff --git a/assets/admin/src/schema/sections/forum/basic.js b/assets/admin/src/schema/sections/forum/basic.js deleted file mode 100644 index c43a8f14..00000000 --- a/assets/admin/src/schema/sections/forum/basic.js +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 40133c99..00000000 --- a/assets/admin/src/schema/sections/forum/board-page.js +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index ee38c282..00000000 --- a/assets/admin/src/schema/sections/forum/credits.js +++ /dev/null @@ -1,37 +0,0 @@ -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_hot_recommend_threshold: 500, - 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_hot_recommend_threshold', label: '热帖推荐分阈值', type: 'number', default: 500, min: 1, help: '推荐分达到该值触发热帖奖励与勋章累计' }, - { 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 deleted file mode 100644 index e36b0c2a..00000000 --- a/assets/admin/src/schema/sections/forum/homepage.js +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index d0396a9d..00000000 --- a/assets/admin/src/schema/sections/forum/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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 deleted file mode 100644 index 80f99d72..00000000 --- a/assets/admin/src/schema/sections/forum/names.js +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index d0c8c20a..00000000 --- a/assets/admin/src/schema/sections/forum/other.js +++ /dev/null @@ -1,64 +0,0 @@ -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: true, - 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: true }, - { 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 deleted file mode 100644 index 25c227c2..00000000 --- a/assets/admin/src/schema/sections/forum/permissions.js +++ /dev/null @@ -1,64 +0,0 @@ -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: true, - bbs_show_new_topic: true, - bbs_show_apply_moderator: true, - bbs_post_pay_hide_type_s: true, - bbs_post_pay_vip_price_s: true, - 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: true }, - { 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: true }, - { key: 'bbs_post_pay_vip_price_s', label: 'VIP 特价', type: 'switch', default: true, 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 deleted file mode 100644 index 3cd1aa2a..00000000 --- a/assets/admin/src/schema/sections/forum/post-page.js +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index e9235b3d..00000000 --- a/assets/admin/src/schema/sections/forum/reply.js +++ /dev/null @@ -1,44 +0,0 @@ -export const replySchema = { - key: 'forum.reply', - title: '回复评论', - icon: 'fas fa-reply-all', - description: '回复翻页、工具栏功能、表情、代码和快捷回复配置。', - defaults: { - bbs_reply_paginate_type: 'ajax', - bbs_reply_paginate_ias_s: true, - bbs_reply_paginate_ias_max: 3, - bbs_comment_smilie: true, - bbs_comment_code: true, - bbs_comment_img: true, - bbs_comment_upload_img: true, - 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: true }, - { 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: true }, - { 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 deleted file mode 100644 index df879eaa..00000000 --- a/assets/admin/src/schema/sections/forum/upload.js +++ /dev/null @@ -1,27 +0,0 @@ -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/search.js b/assets/admin/src/schema/sections/global/search.js index 3ae14cf7..4b6fe809 100644 --- a/assets/admin/src/schema/sections/global/search.js +++ b/assets/admin/src/schema/sections/global/search.js @@ -25,8 +25,6 @@ export const searchSchema = { 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: '', @@ -63,8 +61,6 @@ export const searchSchema = { { 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: '默认类型', @@ -75,8 +71,6 @@ export const searchSchema = { { label: '用户', value: 'user' }, { label: '分类', value: 'category' }, { label: '标签', value: 'tag' }, - { label: '商品', value: 'product' }, - { label: '帖子', value: 'topic' }, ], }, ], diff --git a/assets/admin/src/schema/sections/page-display/index.js b/assets/admin/src/schema/sections/page-display/index.js index 0e0f45f3..27de3175 100644 --- a/assets/admin/src/schema/sections/page-display/index.js +++ b/assets/admin/src/schema/sections/page-display/index.js @@ -136,15 +136,9 @@ export const pageDisplaySchemas = { ], dependsOn: { key: 'header_newposts_enabled', value: true } }, { key: 'header_newposts_types', label: '发布类型', type: 'checkbox', default: ['post'], options: [ { label: '文章', value: 'post' }, - { label: '帖子', value: 'topic' }, - { label: '商品', value: 'product' }, - { label: '版块', value: 'plate' }, ], dependsOn: { key: 'header_newposts_enabled', value: true }, help: '未开启的模块不会显示' }, { key: 'header_user_newposts_btn', label: '用户卡发布类型', type: 'select', default: 'post', options: [ { label: '文章', value: 'post' }, - { label: '帖子', value: 'topic' }, - { label: '商品', value: 'product' }, - { label: '版块', value: 'plate' }, ] }, { key: 'header_user_newposts_text', label: '用户卡发布文案', type: 'input', default: '发布内容' }, { key: 'header_nav_pay_vip', label: '导航栏开通会员按钮', type: 'switch', default: true, help: '顶部导航显示开通会员' }, @@ -349,7 +343,7 @@ export const pageDisplaySchemas = { title: 'Tab 配置', icon: 'fas fa-columns', fields: [ - { key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum' }, + { key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits' }, { key: 'user_home_tabs_order', label: 'Tab 显示顺序', type: 'input', default: 'posts,favorites,comments,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: '逗号分隔要在移动端隐藏的栏目' }, @@ -669,9 +663,6 @@ export const pageDisplaySchemas = { { label: '用户页', value: 'author' }, { label: '文章页', value: 'single' }, { label: '搜索页', value: 'search' }, - { label: '[论坛]首页', value: 'forum_home' }, - { label: '[论坛]板块页面', value: 'forum_plate' }, - { label: '[论坛]帖子页面', value: 'forum_post' }, { label: '其它页面', value: 'page' }, ], help: '选择启用顶部多功能组件的页面类型,一个都不选则为关闭此功能' }, { key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [ @@ -717,13 +708,10 @@ export const pageDisplaySchemas = { { key: 'header_components_search_popular_limit', label: '关键词最多显示', type: 'number', default: 6, min: 2, max: 50, 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_in_cat', label: '默认搜索的分类', type: 'input', default: '', dependsOn: { key: 'header_components_search_show_input_cat', value: true }, help: '分类 ID,可留空' }, - { key: 'header_components_search_show_type', 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: 'header_components_search_in_type', label: '默认搜索类型', type: 'radio', default: 'post', dependsOn: { key: 'header_components_search_show_type', value: true }, options: [ { label: '文章', value: 'post' }, { label: '用户', value: 'user' }, - { label: '商品', value: 'product' }, - { label: '帖子', value: 'forum' }, - { label: '版块', value: 'plate' }, ]}, { key: 'header_components_search_before_html', diff --git a/assets/admin/src/schema/sections/shop/basic.js b/assets/admin/src/schema/sections/shop/basic.js deleted file mode 100644 index 2d365640..00000000 --- a/assets/admin/src/schema/sections/shop/basic.js +++ /dev/null @@ -1,49 +0,0 @@ -export const basicSchema = { - key: 'shop.basic', - title: '商城基础', - icon: 'fas fa-store', - description: '商城总开关、首页链接、面包屑和 URL 别名设置。', - defaults: { - shop_s: true, - shop_home_url: '', - shop_breadcrumbs_s: true, - shop_breadcrumbs_home: true, - shop_breadcrumbs_shop_home: false, - shop_breadcrumbs_shop_home_name: '商城首页', - shop_product_rewrite_slug: 'product', - shop_cart_rewrite_slug: 'cart', - shop_rewrite_suffix_html_s: true, - }, - groups: [ - { - key: 'general', - title: '商城开关', - icon: 'fas fa-power-off', - fields: [ - { key: 'shop_s', label: '启用商城', type: 'switch', default: true }, - { 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: false, 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: true, 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 deleted file mode 100644 index 6a1fc171..00000000 --- a/assets/admin/src/schema/sections/shop/contact.js +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index eb8f945e..00000000 --- a/assets/admin/src/schema/sections/shop/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * 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 deleted file mode 100644 index 6074ddd7..00000000 --- a/assets/admin/src/schema/sections/shop/list.js +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 566b6bbc..00000000 --- a/assets/admin/src/schema/sections/shop/product-defaults.js +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 9e41c82d..00000000 --- a/assets/admin/src/schema/sections/shop/shipping-config.js +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index e2056c55..00000000 --- a/assets/admin/src/schema/sections/shop/single.js +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 76c40067..00000000 --- a/assets/admin/src/schema/sections/shop/timing.js +++ /dev/null @@ -1,54 +0,0 @@ -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/users/badge.js b/assets/admin/src/schema/sections/users/badge.js index 7d1f73cb..9e12cdb1 100644 --- a/assets/admin/src/schema/sections/users/badge.js +++ b/assets/admin/src/schema/sections/users/badge.js @@ -7,16 +7,10 @@ export const MEDAL_GET_TYPES = [ { label: '注册天数', value: 'registration_time' }, { label: '累计签到天数', value: 'checkin_all_day' }, { label: '发布文章数', value: 'new_post' }, - { label: '论坛发帖数', value: 'bbs_post' }, { label: '评论数', value: 'comment' }, { label: '粉丝数', value: 'followed' }, { label: '文章获赞', value: 'like' }, { label: '评论获赞', value: 'comment_like' }, - { label: '回复被采纳', value: 'bbs_adopt' }, - { label: '神评次数', value: 'bbs_comment_hot' }, - { label: '热帖次数', value: 'bbs_post_hot' }, - { label: '精华帖数', value: 'bbs_essence' }, - { label: '帖子评分累计', value: 'bbs_score' }, { label: '内容阅读量', value: 'views' }, { label: '创作分成累计(元)', value: 'pay_income' }, { label: '推广佣金累计(元)', value: 'pay_rebate' }, diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index 09ff0727..637c9a81 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -19,13 +19,6 @@ export const creditsSchema = { points_free_comment_new: 1, points_free_comment_like: 1, points_free_followed: 1, - points_free_bbs_posts_new: 5, - points_free_bbs_reply: 2, - points_free_bbs_essence: 10, - points_free_bbs_adopt: 20, - points_free_bbs_posts_hot: 5, - points_free_bbs_comment_hot: 5, - points_free_bbs_score_extra: 3, points_earn_invite: 10, points_spend_comment: 0, points_spend_download: 0, @@ -74,13 +67,6 @@ export const creditsSchema = { { key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 }, { key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 }, { key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 }, - { key: 'points_free_bbs_posts_new', label: '论坛发帖', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_reply', label: '论坛回复', type: 'number', default: 2, min: 0, max: 1000 }, - { key: 'points_free_bbs_essence', label: '帖子加精', type: 'number', default: 10, min: 0, max: 1000 }, - { key: 'points_free_bbs_adopt', label: '回复被采纳', type: 'number', default: 20, min: 0, max: 1000 }, - { key: 'points_free_bbs_posts_hot', label: '热帖奖励', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_comment_hot', label: '神评奖励', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_score_extra', label: '帖子加分', type: 'number', default: 3, min: 0, max: 1000 }, { key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' }, ], }, diff --git a/assets/admin/src/schema/sections/users/level.js b/assets/admin/src/schema/sections/users/level.js index 22003336..f0c38ba4 100644 --- a/assets/admin/src/schema/sections/users/level.js +++ b/assets/admin/src/schema/sections/users/level.js @@ -11,14 +11,6 @@ export const INTEGRAL_CATALOG = [ { key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' }, { key: 'comment_new', label: '发表评论', default: 2, group: '文章' }, { key: 'comment_like', label: '评论获赞', default: 1, group: '文章' }, - { key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' }, - { key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' }, - { key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' }, - { key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' }, - { key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' }, - { key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' }, - { key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' }, - { key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' }, ] export function defaultLevelOpt(max = 10) { diff --git a/assets/css/frontend/forum.css b/assets/css/frontend/forum.css deleted file mode 100644 index 65e303ec..00000000 --- a/assets/css/frontend/forum.css +++ /dev/null @@ -1,76 +0,0 @@ -@import url("forum/board.css"); -@import url("forum/topic.css"); -@import url("forum/compose.css"); -@import url("forum/extras.css"); - - -/* Forum home shell: sidebar compose CTA */ -.aksrt-forum-sidebar-card--compose { - padding: 12px; -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 10px 12px; - border-radius: 10px; - background: var(--body-bg-color, #f5f6f7); - color: var(--muted-color, #777); - text-decoration: none; -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input:hover { - color: var(--focus-color, #f04494); -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-header__action { - display: inline-flex; - width: 100%; - justify-content: center; -} -.aksrt-forum-header__stats { - display: none; /* stats demoted on home shell; counts remain in eyebrow meta */ -} - - -/* Forum pages share the single/archive content stream shell */ -.content-shell--forum .content-wrap { - padding-top: 8px; -} -.content-shell--forum .content-layout__primary > .aksrt-forum-board, -.content-shell--forum .content-layout__primary > .aksrt-forum-topic, -.content-shell--forum .content-layout__primary > .aksrt-forum-tag, -.content-shell--forum .content-layout__primary > .aksrt-forum-plate-cat { - width: 100%; -} - - -.aksrt-forum-topic__cover { - margin: 0 0 16px; - border-radius: var(--main-radius, 8px); - overflow: hidden; - background: var(--body-bg-color, #f5f6f7); -} -.aksrt-forum-topic__cover--image { - aspect-ratio: var(--cover-ratio, 16/9); -} -.aksrt-forum-topic__cover img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} -.aksrt-forum-topic__cover--slide { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 100%; - overflow-x: auto; - scroll-snap-type: x mandatory; -} -.aksrt-forum-topic__cover--slide img { - scroll-snap-align: start; - aspect-ratio: 16/9; -} -.aksrt-forum-topic__paywall { - margin: 12px 0 20px; - padding: 16px; -} diff --git a/assets/css/frontend/forum/board.css b/assets/css/frontend/forum/board.css deleted file mode 100644 index df23dd1c..00000000 --- a/assets/css/frontend/forum/board.css +++ /dev/null @@ -1,755 +0,0 @@ -/* Forum home-shell refinement */ -.aksrt-forum-board__hero { - margin-bottom: 20px; - padding: 24px; - border-radius: var(--radius-2xl, 16px); - background: linear-gradient(180deg, rgba(240,68,148,0.05), rgba(255,255,255,0.96)); - border: none; - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__summary { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 16px; -} - -.aksrt-forum-board__summary-item { - min-width: 120px; - padding: 10px 14px; - border-radius: 999px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); -} - -.aksrt-forum-board__summary-label { - display: block; - margin-bottom: 4px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__layout { - align-items: start; -} - -.aksrt-forum-board__governance--inline { - margin-bottom: 20px; -} - -.aksrt-forum-board__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -.aksrt-forum-board__governance-card--sidebar { - position: sticky; - top: calc(var(--nav-height, 64px) + 16px); -} - -.aksrt-forum-board__moderators--stacked { - flex-direction: column; - align-items: stretch; -} - -.aksrt-forum-board { - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-board__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - margin-bottom: 24px; - padding: 22px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-forum-board__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__meta { - display: flex; - gap: 16px; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__meta i { - margin-right: 4px; -} - -.aksrt-forum-board__compose-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 18px; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - text-decoration: none; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - transition: opacity 0.15s; -} - -.aksrt-forum-board__compose-btn:hover { - opacity: 0.85; -} - -.aksrt-forum-board__actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: flex-end; -} - -.aksrt-forum-board__manage-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 8px; - border: 1px solid var(--color-line, #d9d9d9); - background: var(--surface-1, #fff); - color: var(--color-ink); - text-decoration: none; - cursor: pointer; - font-size: 13px; -} - -.aksrt-forum-board__manage-btn--danger { - border-color: var(--color-error, #f5222d); - color: var(--color-error, #f5222d); -} - -.aksrt-forum-board__manage-btn--danger:hover { - background: var(--color-error, #f5222d); - color: #fff; -} - -.aksrt-forum-board__governance { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 16px; - margin-bottom: 20px; -} - -.aksrt-forum-board__governance-card { - padding: 18px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__section-title { - margin: 0 0 10px; - font-size: 15px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-forum-board__moderators { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.aksrt-forum-board__moderator { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - border-radius: 999px; - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-board__moderator img { - width: 36px; - height: 36px; - border-radius: 50%; -} - -.aksrt-forum-board__application-form textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - resize: vertical; - font-family: inherit; -} - -.aksrt-forum-board__application-actions { - display: flex; - align-items: center; - gap: 10px; - margin-top: 10px; -} - -.aksrt-forum-board__apply-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 8px 14px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; -} - -.aksrt-forum-board__application-note { - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__application-reason { - margin: 10px 0; - padding: 10px 12px; - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); - background: var(--surface-2, #f8fafc); - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-board__governance-card .aksrt-forum-board__apply-btn { - text-decoration: none; -} - -/* Sticky section */ -.aksrt-forum-board__sticky { - margin-bottom: 16px; - padding: 12px 16px; - background: var(--surface-1, #fff); - border-radius: 8px; - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); -} - -.aksrt-forum-board__sticky h3 { - margin: 0 0 8px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__sticky-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 0; - text-decoration: none; - color: var(--color-ink); - font-size: 14px; -} - -.aksrt-forum-board__sticky-item:hover { - color: var(--color-primary); -} - -.aksrt-forum-board__sticky-meta { - font-size: 12px; - color: var(--color-ink-4, #c0c4cc); -} - -.aksrt-forum-board__sticky-badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 11px; - font-weight: 500; - background: var(--color-primary); - color: #fff; - margin-right: 6px; - flex-shrink: 0; -} - -/* Topic list items */ -.aksrt-forum-board__topics { - display: flex; - flex-direction: column; - gap: 1px; -} - -.aksrt-forum-topic-item { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 18px; - background: var(--surface-1, #fff); - border-bottom: 1px solid var(--color-line-soft); - transition: background 0.15s, box-shadow 0.15s; -} - -.aksrt-forum-topic-item:first-child { - border-radius: 8px 8px 0 0; -} - -.aksrt-forum-topic-item:last-child { - border-radius: 0 0 8px 8px; -} - -.aksrt-forum-topic-item:hover { - background: var(--surface-4, #f8f9fa); -} - -.aksrt-forum-topic-item.is-sticky { - background: var(--surface-2, #f0f5ff); -} - -.aksrt-forum-topic-item.is-featured { - border-left: 3px solid var(--color-success, #52c41a); -} - -.aksrt-forum-topic-item__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; -} - -.aksrt-forum-topic-item__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-item__title { - font-size: 15px; - font-weight: 500; - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic-item__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-item__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-item__badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 11px; - font-weight: 500; -} - -.aksrt-forum-topic-item__badge--sticky { - background: var(--color-primary); - color: #fff; -} - -/* 置顶分级视觉区分(§7.3):超级置顶加深色,全局置顶用红色凸显。 */ -.aksrt-forum-topic-item__badge--sticky-super { - background: var(--focus-color, #fd53a1); - color: #fff; -} - -.aksrt-forum-topic-item__badge--sticky-global { - background: #e53e3e; - color: #fff; -} - -.aksrt-forum-topic-item__badge--featured { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-topic-item__badge--muted { - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic-item__lock { - color: var(--color-warning, #fa8c16); - font-size: 12px; -} - -.aksrt-forum-topic-item__meta { - display: flex; - gap: 12px; - margin-top: 3px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-item__stats { - display: flex; - gap: 12px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-item__stats i { - margin-right: 3px; -} - -.aksrt-forum-topic-item__actions { - display: flex; - flex-direction: column; - gap: 6px; -} - -.aksrt-forum-topic-item__action { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 6px 10px; - border-radius: 999px; - border: 1px solid var(--color-line, #d9d9d9); - background: #fff; - color: var(--color-ink); - text-decoration: none; - cursor: pointer; - font-size: 12px; - white-space: nowrap; -} - -.aksrt-forum-topic-item__action--link { - background: var(--surface-1, #f8f9fa); -} - -/* Pagination */ -.aksrt-forum-board__pagination { - display: flex; - justify-content: center; - padding: 20px 0; -} - -/* Empty state */ -.aksrt-forum-board__empty { - text-align: center; - padding: 48px 24px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__empty i { - font-size: 40px; - margin-bottom: 12px; - display: block; -} - -.aksrt-forum-board__empty a { - display: inline-block; - margin-top: 12px; - color: var(--color-primary); -} - - -/* Board sort tabs */ -.aksrt-forum-board__tabs { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 16px; - padding: 8px; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__tab { - display: inline-flex; - align-items: center; - padding: 8px 14px; - border-radius: 999px; - color: var(--color-ink-3, #5c6370); - text-decoration: none; - font-size: 14px; - font-weight: 600; - transition: background .15s ease, color .15s ease; -} - -.aksrt-forum-board__tab:hover { - background: rgba(240, 68, 148, 0.08); - color: var(--color-primary, #f04494); -} - -.aksrt-forum-board__tab.is-active { - background: var(--color-primary, #f04494); - color: #fff; -} - -.aksrt-forum-board__governance--footer { - margin-top: 28px; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 16px; -} - -.aksrt-forum-topic-row { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 14px 16px; - margin-bottom: 10px; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic-row__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; -} - -.aksrt-forum-topic-row__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-row__title-wrap { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic-row__title { - margin: 0; - font-size: 16px; - font-weight: 700; -} - -.aksrt-forum-topic-row__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-row__meta { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 6px; - color: var(--color-ink-4, #8a909c); - font-size: 12px; -} - -.aksrt-forum-topic-row__badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 999px; - font-size: 12px; - font-weight: 700; - line-height: 1.4; -} - -.aksrt-forum-topic-row__badge--sticky { background: rgba(255, 149, 0, 0.12); color: #c77700; } -.aksrt-forum-topic-row__badge--featured { background: rgba(240, 68, 148, 0.12); color: #f04494; } -.aksrt-forum-topic-row__badge--question { background: rgba(47, 128, 237, 0.12); color: #2f80ed; } -.aksrt-forum-topic-row__badge--question.is-solved { background: rgba(39, 174, 96, 0.12); color: #27ae60; } - -.aksrt-forum-topic-row__cover { - position: relative; - width: 84px; - height: 60px; - flex: 0 0 auto; - border-radius: 10px; - overflow: hidden; - background: var(--surface-2, #f5f6f8); -} - -.aksrt-forum-topic-row__cover img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.aksrt-forum-topic-row__cover .fa-play-circle, -.aksrt-forum-topic-row__cover-count { - position: absolute; - right: 6px; - bottom: 6px; - color: #fff; - text-shadow: 0 1px 2px rgba(0,0,0,.35); -} - -/* detail list card (zibll forum-posts detail) */ -.aksrt-forum-topic-row.detail, -.aksrt-forum-posts.detail { - display: block; - padding: 16px; - margin-bottom: 12px; - border-radius: 14px; - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm, 0 1px 4px rgba(0,0,0,.06)); -} -.aksrt-forum-topic-row__author-card { gap: 10px; } -.aksrt-forum-topic-row__author-name { font-weight: 700; font-size: 14px; } -.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover, -.aksrt-forum-posts.detail .forum-lists-cover { - display: block; - width: 100%; - height: auto; - max-height: 280px; - margin: 0 0 12px; - border-radius: 12px; - overflow: hidden; -} -.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover img { - width: 100%; - height: auto; - max-height: 280px; - object-fit: cover; - display: block; -} -.aksrt-forum-topic-row .excerpt { - margin-top: 6px; - font-size: 14px; - line-height: 1.65; - color: var(--color-ink-2, #4b5563); -} -.aksrt-forum-topic-row .lists-imgs { - display: grid; - gap: 6px; - margin-top: 8px; -} -.aksrt-forum-topic-row .lists-imgs.count-1 { grid-template-columns: 1fr; max-width: 280px; } -.aksrt-forum-topic-row .lists-imgs.count-2 { grid-template-columns: 1fr 1fr; } -.aksrt-forum-topic-row .lists-imgs.count-3, -.aksrt-forum-topic-row .lists-imgs.count-4 { grid-template-columns: 1fr 1fr; } -.aksrt-forum-topic-row .lists-imgs__item { - display: block; - border-radius: 10px; - overflow: hidden; - aspect-ratio: 1; - background: var(--surface-2, #f3f4f6); -} -.aksrt-forum-topic-row .lists-imgs__item img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} -.aksrt-forum-topic-row__vote { display: grid; gap: 6px; } -.aksrt-forum-topic-row__vote-opt { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(80px, 2fr) 42px; - gap: 8px; - align-items: center; - font-size: 13px; -} -.aksrt-forum-topic-row__vote-bar { - height: 8px; - border-radius: 999px; - background: var(--surface-4, #eef0f3); - overflow: hidden; -} -.aksrt-forum-topic-row__vote-bar i { - display: block; - height: 100%; - background: var(--color-primary, #f04494); -} -.aksrt-forum-topic-row__footer .but-plate, -.aksrt-forum-topic-row__board.but { - display: inline-flex; - align-items: center; - max-width: 46%; - padding: 4px 10px; - border-radius: 999px; - background: var(--surface-4, #f3f4f6); - color: var(--color-ink-2, #5c6370); - text-decoration: none; - font-size: 12px; - font-weight: 600; -} -.aksrt-forum-topic-row__actions { - display: inline-flex; - gap: 12px; - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} -.aksrt-forum-topic-row__actions a { color: inherit; text-decoration: none; } -.aksrt-forum-topic-row.mini { display: flex; gap: 12px; align-items: flex-start; } - -.aksrt-forum-board-card__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 12px; -} - -.aksrt-forum-board-card { - display: flex; - flex-direction: column; - overflow: hidden; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); - text-decoration: none; - color: inherit; -} - -.aksrt-forum-board-card__cover { - display: block; - height: 96px; - background-size: cover; - background-position: center; -} - -.aksrt-forum-board-card__cover--empty { - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, rgba(240,68,148,.08), rgba(47,128,237,.08)); - color: var(--color-primary, #f04494); - font-size: 24px; -} - -.aksrt-forum-board-card__body { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px 14px 14px; -} - -.aksrt-forum-board-card__title { - font-weight: 700; -} - -.aksrt-forum-board-card__desc, -.aksrt-forum-board-card__meta { - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum__empty, -.aksrt-forum-board__empty, -.aksrt-forum-tag__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - padding: 48px 20px; - text-align: center; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum__empty a, -.aksrt-forum-board__empty a { - color: var(--color-primary, #f04494); - font-weight: 600; - text-decoration: none; -} diff --git a/assets/css/frontend/forum/compose.css b/assets/css/frontend/forum/compose.css deleted file mode 100644 index 77710245..00000000 --- a/assets/css/frontend/forum/compose.css +++ /dev/null @@ -1,714 +0,0 @@ -/* ============================================================ - * Compose page - * ============================================================ */ - -.aksrt-forum-compose { - padding-top: 24px; - padding-bottom: 48px; - max-width: 1100px; - margin: 0 auto; -} - -.aksrt-forum-compose__header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; -} - -.aksrt-forum-compose__header h1 { - margin: 0; - font-size: 22px; -} - -.aksrt-forum-compose__notice { - margin: 8px 0 0; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-compose__back { - color: var(--color-ink-3, #8a909c); - text-decoration: none; - font-size: 14px; -} - -.aksrt-forum-compose__back:hover { - color: var(--color-primary); -} - -/* §7.4 — Two-column layout: main + sidebar */ -.aksrt-forum-compose__layout { - display: grid; - grid-template-columns: 1fr 311px; - gap: 24px; - align-items: start; -} - -.aksrt-forum-compose__main { - min-width: 0; -} - -.aksrt-forum-compose__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* §7.4 — Sidebar panels */ -.aksrt-forum-compose__panel { - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__panel-title { - margin: 0; - padding: 12px 16px; - font-size: 14px; - font-weight: 600; - color: var(--color-ink); - border-bottom: 1px solid var(--color-line, #eceff4); - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-compose__panel-title i { - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-compose__panel-body { - padding: 14px 16px; -} - -.aksrt-forum-compose__readonly { - margin: 0; - color: var(--color-ink-2, #555); - font-size: 14px; -} - -.aksrt-forum-compose__status-line { - margin: 0 0 12px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-compose__form { - padding: 0; - background: transparent; - border-radius: 0; - box-shadow: none; -} - -.aksrt-forum-compose__main { - padding: 24px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__field { - margin-bottom: 16px; -} - -.aksrt-forum-compose__field:last-child { - margin-bottom: 0; -} - -.aksrt-forum-compose__field label { - display: block; - font-size: 14px; - font-weight: 500; - color: var(--color-ink); - margin-bottom: 6px; -} - -.aksrt-forum-compose__field .required { - color: var(--color-error, #f5222d); -} - -.aksrt-forum-compose__field select, -.aksrt-forum-compose__field input[type="text"], -.aksrt-forum-compose__field input[type="number"], -.aksrt-forum-compose__field input[type="url"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 8px 12px; - font-size: 14px; - font-family: inherit; - box-sizing: border-box; -} - -.aksrt-forum-compose__field textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - resize: vertical; - font-family: inherit; - box-sizing: border-box; -} - -.aksrt-forum-compose__checkbox { - display: inline-flex; - align-items: center; - gap: 8px; - cursor: pointer; -} - -/* §7.4 — wp_editor wrapper */ -.aksrt-forum-compose__editor-wrap { - margin-bottom: 0; -} - -.aksrt-forum-compose__editor-wrap .wp-editor-wrap { - border-radius: 8px; - overflow: hidden; -} - -.aksrt-forum-compose__editor-wrap .wp-editor-container { - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; -} - -/* §7.4 — Character counter */ -.aksrt-forum-compose__char-counter { - display: block; - text-align: right; - font-size: 12px; - color: var(--color-ink-3, #8a909c); - margin-top: 4px; -} - -.aksrt-forum-compose__char-counter.is-near { - color: var(--color-warning, #d48806); -} - -.aksrt-forum-compose__char-counter.is-over { - color: var(--color-error, #cf1322); - font-weight: 600; -} - -/* §7.4 — Draft notice */ -.aksrt-forum-compose__draft-notice { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-bottom: 20px; - padding: 12px 16px; - background: var(--color-info-bg, #e6f7ff); - border: 1px solid var(--color-info-border, #91d5ff); - border-radius: 8px; - font-size: 14px; - color: var(--color-info, #0050b3); -} - -.aksrt-forum-compose__draft-toggle { - border: none; - background: none; - color: var(--color-primary); - cursor: pointer; - font-size: 13px; - display: inline-flex; - align-items: center; - gap: 4px; - padding: 0; -} - -.aksrt-forum-compose__draft-list { - width: 100%; - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 6px; -} - -.aksrt-forum-compose__draft-item { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: var(--surface-1, #fff); - border-radius: 6px; - font-size: 13px; -} - -.aksrt-forum-compose__draft-title { - flex: 1; - color: var(--color-ink); - text-decoration: none; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.aksrt-forum-compose__draft-title:hover { - color: var(--color-primary); -} - -.aksrt-forum-compose__draft-meta { - color: var(--color-ink-3, #8a909c); - font-size: 12px; - flex-shrink: 0; -} - -.aksrt-forum-compose__draft-delete { - border: none; - background: none; - color: var(--color-ink-3, #8a909c); - cursor: pointer; - padding: 2px; - font-size: 12px; -} - -.aksrt-forum-compose__draft-delete:hover { - color: var(--color-error, #cf1322); -} - -/* §7.4 — Remote search select */ -.aksrt-remote-select { - position: relative; -} - -.aksrt-remote-select__input { - width: 100%; -} - -.aksrt-remote-select__dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 100; - background: var(--surface-1, #fff); - border: none; - border-top: none; - border-radius: 0 0 12px 12px; - max-height: 200px; - overflow-y: auto; - box-shadow: var(--shadow-lg); -} - -.aksrt-remote-select__option { - padding: 8px 12px; - cursor: pointer; - font-size: 14px; -} - -.aksrt-remote-select__option:hover { - background: var(--color-primary-bg, #e6f7ff); -} - -/* §7.4 — Tag tokenizer */ -.aksrt-tag-input { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - padding: 8px 10px; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 12px; - background: var(--surface-4, #fff); - min-height: 42px; -} - -.aksrt-tag-input__tokens { - display: flex; - flex-wrap: wrap; - gap: 4px; -} - -.aksrt-tag-input__token { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - background: var(--color-primary-bg, #e6f7ff); - color: var(--color-primary, #1890ff); - border-radius: 4px; - font-size: 13px; -} - -.aksrt-tag-input__token-remove { - border: none; - background: none; - color: var(--color-primary, #1890ff); - cursor: pointer; - padding: 0; - font-size: 14px; - line-height: 1; - opacity: 0.7; -} - -.aksrt-tag-input__token-remove:hover { - opacity: 1; -} - -.aksrt-tag-input__input { - flex: 1; - min-width: 80px; - border: none !important; - padding: 2px 4px !important; - font-size: 13px; - outline: none; - box-shadow: none !important; -} - -.aksrt-tag-input__dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 100; - background: var(--surface-1, #fff); - border: none; - border-top: none; - border-radius: 0 0 12px 12px; - max-height: 160px; - overflow-y: auto; - box-shadow: var(--shadow-lg); -} - -.aksrt-tag-input__suggestion { - padding: 6px 12px; - cursor: pointer; - font-size: 13px; -} - -.aksrt-tag-input__suggestion:hover { - background: var(--color-primary-bg, #e6f7ff); -} - -.aksrt-forum-compose__vote-fields[hidden] { - display: none; -} - -.aksrt-forum-compose__vote { - margin-bottom: 20px; - padding: 18px; - border: none; - border-radius: var(--radius-2xl, 16px); - background: var(--surface-1, #f8f9fa); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__vote-head { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 12px; -} - -.aksrt-forum-compose__vote-head p { - margin: 0; - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-compose__feedback, -.aksrt-forum-board__application-feedback, -.aksrt-forum-topic__vote-feedback, -.aksrt-forum-topic__moderation-feedback { - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-compose__feedback.is-success, -.aksrt-forum-board__application-feedback.is-success, -.aksrt-forum-topic__vote-feedback.is-success, -.aksrt-forum-topic__moderation-feedback.is-success { - color: var(--color-success, #389e0d); -} - -.aksrt-forum-compose__feedback.is-error, -.aksrt-forum-board__application-feedback.is-error, -.aksrt-forum-topic__vote-feedback.is-error, -.aksrt-forum-topic__moderation-feedback.is-error { - color: var(--color-error, #cf1322); -} - -.aksrt-forum-compose__feedback.is-loading, -.aksrt-forum-board__application-feedback.is-loading, -.aksrt-forum-topic__vote-feedback.is-loading, -.aksrt-forum-topic__moderation-feedback.is-loading { - color: var(--color-primary); -} - -.aksrt-forum-compose__actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 12px; -} - -.aksrt-forum-compose__submit { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 24px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; - font-size: 15px; - font-weight: 500; -} - -.aksrt-forum-compose__submit:hover { - opacity: 0.85; -} - -.aksrt-forum-compose__submit:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.aksrt-forum-compose__draft { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 24px; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - background: var(--surface-1, #fff); - color: var(--color-ink-3, #8a909c); - cursor: pointer; - font-size: 15px; -} - -.aksrt-forum-compose__draft:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - background: var(--color-primary-softer, color-mix(in srgb, var(--color-primary) 6%, transparent)); -} - -/* ── topic cover (§7.3) ── */ - -.aksrt-forum-topic__cover { - margin-bottom: 20px; - border-radius: 12px; - overflow: hidden; - background: var(--surface-2, #f5f7fa); -} - -.aksrt-forum-topic__cover-img { - width: 100%; - display: block; - object-fit: cover; -} - -.aksrt-forum-topic__cover-slides { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; -} - -.aksrt-forum-topic__cover-slides img { - width: 100%; - aspect-ratio: 1; - object-fit: cover; -} - -.aksrt-forum-topic__cover-player { - position: relative; - cursor: pointer; -} - -.aksrt-forum-topic__cover-poster { - width: 100%; - display: block; - object-fit: cover; -} - -.aksrt-forum-topic__cover-play-btn { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border: none; - background: rgba(0, 0, 0, 0.5); - color: #fff; - font-size: 48px; - border-radius: 50%; - width: 72px; - height: 72px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: background 0.15s; -} - -.aksrt-forum-topic__cover-play-btn:hover { - background: rgba(0, 0, 0, 0.7); -} - -/* ── topic list cover thumbnails (§7.3) ── */ - -.aksrt-forum-topic-item__cover { - flex-shrink: 0; - width: 120px; - height: 80px; - border-radius: 8px; - overflow: hidden; - background: var(--surface-2, #f5f7fa); -} - -.aksrt-forum-topic-item__cover img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.aksrt-forum-topic-item__cover-video, -.aksrt-forum-topic-item__cover-slide { - position: relative; - width: 100%; - height: 100%; -} - -.aksrt-forum-topic-item__cover-video img, -.aksrt-forum-topic-item__cover-slide img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.aksrt-forum-topic-item__cover-video i { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 24px; - color: #fff; - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); -} - -.aksrt-forum-topic-item__cover-count { - position: absolute; - bottom: 4px; - right: 4px; - padding: 1px 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.55); - color: #fff; - font-size: 11px; -} - -/* ── compose cover upload (§7.3) ── */ - -.aksrt-forum-compose__cover-upload { - margin-bottom: 20px; -} - -.aksrt-forum-compose__cover-preview { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 10px; - min-height: 60px; -} - -.aksrt-forum-compose__cover-preview img { - width: 120px; - height: 80px; - object-fit: cover; - border-radius: 12px; - border: none; - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__cover-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 18px; - border: 1px dashed var(--color-line, #d9d9d9); - border-radius: 12px; - background: var(--surface-4, #fff); - color: var(--color-ink-3, #8a909c); - cursor: pointer; - font-size: 14px; - transition: border-color 0.15s, color 0.15s, background 0.15s; -} - -.aksrt-forum-compose__cover-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - background: var(--color-primary-soft); -} - -.aksrt-forum-compose__field input[type="url"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - font-family: inherit; -} - -@media (max-width: 767px) { - .aksrt-forum-board__header, - .aksrt-forum-board__actions, - .aksrt-forum-topic__vote-head, - .aksrt-forum-topic__moderation-head, - .aksrt-forum-topic__vote-actions, - .aksrt-forum-topic__moderation-actions, - .aksrt-forum-compose__header, - .aksrt-forum-compose__actions { - flex-direction: column; - align-items: stretch; - } - - .aksrt-forum-topic-item { - flex-wrap: wrap; - } - - .aksrt-forum-topic-item__actions { - width: 100%; - flex-direction: row; - flex-wrap: wrap; - } - - .aksrt-forum-topic-item__cover { - width: 80px; - height: 56px; - } - - .aksrt-forum-topic__cover-slides { - grid-template-columns: repeat(2, 1fr); - } - - .aksrt-forum-topic__moderation-grid, - .aksrt-forum-compose__grid { - grid-template-columns: 1fr; - } - - /* §7.4 — Stack sidebar below main on mobile */ - .aksrt-forum-compose__layout { - grid-template-columns: 1fr; - } - - .aksrt-forum-compose__sidebar { - order: 2; - } - - .aksrt-forum-compose { - max-width: 100%; - } -} - diff --git a/assets/css/frontend/forum/extras.css b/assets/css/frontend/forum/extras.css deleted file mode 100644 index 527be719..00000000 --- a/assets/css/frontend/forum/extras.css +++ /dev/null @@ -1,7 +0,0 @@ -@import url("extras/base.css"); -@import url("extras/reply-header-tabs-filters.css"); -@import url("extras/load-more.css"); -@import url("extras/additional-role-badges.css"); -@import url("extras/like-button.css"); -@import url("extras/reply-action-buttons.css"); -@import url("extras/quick-post-bar.css"); diff --git a/assets/css/frontend/forum/extras/additional-role-badges.css b/assets/css/frontend/forum/extras/additional-role-badges.css deleted file mode 100644 index b02887c9..00000000 --- a/assets/css/frontend/forum/extras/additional-role-badges.css +++ /dev/null @@ -1,52 +0,0 @@ -/* --- additional role badges --- */ - -.aksrt-forum-reply__role-badge--author { - background: #fff7e6; - color: #d46b08; -} - -.aksrt-forum-reply__role-badge--super { - background: #f6ffed; - color: #389e0d; -} - -.aksrt-forum-reply__role-badge--catmod { - background: #fff0f6; - color: #c41d7f; -} - -/* --- hot comment (神评) badge --- */ - -.aksrt-forum-reply__badge { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 1px 6px; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - line-height: 1.5; -} - -.aksrt-forum-reply__badge--hot { - background: #fff2e8; - color: #d4380d; -} - -.aksrt-forum-reply__badge--pinned { - background: #e6f7ff; - color: #096dd9; -} - -.aksrt-forum-reply.is-hot { - border-left: 3px solid #fa541c; -} - -.aksrt-forum-reply.is-pinned { - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); -} - -.aksrt-forum-reply.is-pinned.is-hot { - border-left: 3px solid #fa541c; -} - diff --git a/assets/css/frontend/forum/extras/base.css b/assets/css/frontend/forum/extras/base.css deleted file mode 100644 index 645983f7..00000000 --- a/assets/css/frontend/forum/extras/base.css +++ /dev/null @@ -1,621 +0,0 @@ -/* ============================================================ - * Forum tag archive page - * ============================================================ */ - -.aksrt-forum-tag { - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-tag__header { - margin-bottom: 24px; - padding: 20px; - background: var(--surface-1, #fff); - border-radius: 12px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-forum-tag__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; -} - -.aksrt-forum-tag__title i { - color: var(--color-primary); -} - -.aksrt-forum-tag__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-tag__count { - margin: 0; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-tag__list { - display: flex; - flex-direction: column; - gap: 1px; -} - -.aksrt-forum-topic-card { - display: flex; - padding: 16px; - background: var(--surface-1, #fff); - transition: background 0.15s; -} - -.aksrt-forum-topic-card:first-child { - border-radius: 8px 8px 0 0; -} - -.aksrt-forum-topic-card:last-child { - border-radius: 0 0 8px 8px; -} - -.aksrt-forum-topic-card:hover { - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-topic-card__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-card__title { - margin: 0 0 6px; - font-size: 16px; - font-weight: 500; -} - -.aksrt-forum-topic-card__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-card__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-card__excerpt { - margin: 0 0 8px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); - line-height: 1.5; -} - -.aksrt-forum-topic-card__meta { - display: flex; - gap: 16px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-card__meta i { - margin-right: 3px; -} - -.aksrt-forum-tag__pagination { - display: flex; - justify-content: center; - padding: 20px 0; -} - -.aksrt-forum-tag__empty { - text-align: center; - padding: 48px 24px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-tag__empty i { - font-size: 40px; - margin-bottom: 12px; - display: block; -} - -/* ============================================================ - * Forum widgets - * ============================================================ */ - -.aksrt-forum-widget__list { - margin: 0; - padding: 0; - list-style: none; -} - -.aksrt-forum-widget__item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 0; - border-bottom: 1px solid var(--color-line-soft, #f0f1f3); -} - -.aksrt-forum-widget__item:last-child { - border-bottom: none; -} - -.aksrt-forum-widget__rank { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: var(--surface-2, #f4f5f7); - color: var(--color-ink-3, #8a909c); - font-size: 12px; - font-weight: 600; - flex-shrink: 0; -} - -.aksrt-forum-widget__item:nth-child(1) .aksrt-forum-widget__rank { - background: var(--color-error, #ef4444); - color: #fff; -} - -.aksrt-forum-widget__item:nth-child(2) .aksrt-forum-widget__rank { - background: var(--color-warning, #f59e0b); - color: #fff; -} - -.aksrt-forum-widget__item:nth-child(3) .aksrt-forum-widget__rank { - background: var(--color-info, #3b82f6); - color: #fff; -} - -.aksrt-forum-widget__link { - flex: 1; - min-width: 0; - color: var(--color-ink); - text-decoration: none; - font-size: 14px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.aksrt-forum-widget__link:hover { - color: var(--color-primary); -} - -.aksrt-forum-widget__views, -.aksrt-forum-widget__author { - font-size: 12px; - color: var(--color-ink-4, #8a909c); - white-space: nowrap; -} - -.aksrt-forum-widget__boards { - margin: 0; - padding: 0; - list-style: none; -} - -.aksrt-forum-widget__board { - padding: 8px 0; - border-bottom: 1px solid var(--color-line-soft, #f0f1f3); -} - -.aksrt-forum-widget__board:last-child { - border-bottom: none; -} - -.aksrt-forum-widget__board a { - display: flex; - align-items: center; - justify-content: space-between; - color: var(--color-ink); - text-decoration: none; - font-size: 14px; -} - -.aksrt-forum-widget__board a:hover { - color: var(--color-primary); -} - -.aksrt-forum-widget__board a i { - margin-right: 6px; - color: var(--color-primary); -} - -.aksrt-forum-widget__count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 20px; - padding: 0 6px; - background: var(--surface-2, #f4f5f7); - border-radius: var(--radius-pill, 999px); - color: var(--color-ink-3, #8a909c); - font-size: 11px; - font-weight: 600; -} - -.aksrt-forum-widget__empty { - text-align: center; - padding: 16px; - color: var(--color-ink-4, #8a909c); - font-size: 13px; -} - -/* ── bbs_type / question badges (§7.3) ── */ - -.aksrt-forum-topic__badge--question { - background: #e6f7ff; - color: var(--color-primary); -} - -.aksrt-forum-topic__badge--solved { - background: #f6ffed; - color: #52c41a; -} - -.aksrt-forum-topic-item__badge--question { - background: #e6f7ff; - color: var(--color-primary); -} - -.aksrt-forum-topic-item__badge--solved { - background: #f6ffed; - color: #52c41a; -} - -/* ── favorite button (§7.3) ── */ - -.js-aksrt-topic-favorite { - background: none; - border: none; - cursor: pointer; - transition: color .15s; -} - -.js-aksrt-topic-favorite[data-favorited="1"] { - color: var(--color-warning, #faad14); -} - -.js-aksrt-topic-favorite:disabled { - opacity: .5; - cursor: not-allowed; -} - -/* ── score actions (§7.3) ── */ - -.aksrt-forum-topic__score-actions { - display: flex; - align-items: center; - gap: 10px; - margin: 16px 0; - padding: 12px 16px; - background: var(--surface-1, #fff); - border: 1px solid var(--border-2, #e8e8e8); - border-radius: var(--radius-m, 8px); -} - -.aksrt-forum-topic__score-total { - font-size: 14px; - font-weight: 600; - color: var(--color-ink, #333); -} - -.aksrt-forum-topic__score-total i { - color: var(--color-warning, #faad14); - margin-right: 4px; -} - -.aksrt-forum-topic__score-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: 1px solid var(--border-2, #e0e0e0); - border-radius: var(--radius-s, 4px); - background: var(--surface-1, #fff); - cursor: pointer; - font-size: 12px; - transition: all .15s; -} - -.aksrt-forum-topic__score-btn:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - -.aksrt-forum-topic__score-btn--up:hover:not(:disabled) { - color: var(--color-success, #52c41a); - border-color: var(--color-success, #52c41a); -} - -.aksrt-forum-topic__score-btn--down:hover:not(:disabled) { - color: var(--color-error, #f5222d); - border-color: var(--color-error, #f5222d); -} - -.aksrt-forum-topic__score-btn:disabled { - opacity: .4; - cursor: not-allowed; -} - -.aksrt-forum-topic__score-badge { - color: var(--color-warning, #faad14); - font-weight: 600; -} - -.aksrt-forum-topic__score-feedback { - font-size: 13px; - color: var(--color-ink-4, #999); -} - -/* ── vote enhancements (§7.3) ── */ - -.aksrt-forum-topic__vote--pk .aksrt-forum-topic__vote-options { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -.aksrt-forum-topic__vote--expired { - opacity: .7; -} - -.aksrt-forum-topic__vote-type-badge { - display: inline-block; - padding: 1px 6px; - background: var(--color-primary); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 700; - margin-left: 6px; -} - -.aksrt-forum-topic__vote-expired-badge { - display: inline-block; - padding: 1px 6px; - background: var(--color-error, #f5222d); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - margin-left: 6px; -} - -.aksrt-forum-topic__vote-expiry-info { - color: var(--color-ink-4, #999); - font-size: 12px; - margin-left: 6px; -} - -/* ── topic_cat chip (§7.3) ── */ - -.aksrt-forum-topic__chip--topic-cat { - background: var(--color-primary-bg, #e6f7ff); - color: var(--color-primary); - font-weight: 600; -} - -/* ── report modal (§7.5) ── */ - -.aksrt-forum-topic__header-top { - display: flex; - flex-direction: column; - gap: 12px; -} - -.aksrt-forum-report-modal { - position: fixed; - inset: 0; - z-index: 9999; -} - -.aksrt-forum-report-modal__backdrop { - position: absolute; - inset: 0; - background: rgba(15, 23, 42, 0.55); -} - -.aksrt-forum-report-modal__dialog { - position: relative; - max-width: 520px; - margin: 8vh auto 0; - padding: 20px; - border-radius: 14px; - background: var(--surface-1, #fff); - box-shadow: 0 18px 60px rgba(15, 23, 42, 0.2); - z-index: 1; -} - -.aksrt-forum-report-modal__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} - -.aksrt-forum-report-modal__head h2 { - margin: 0; - font-size: 18px; -} - -.aksrt-forum-report-modal__close { - border: none; - background: transparent; - font-size: 24px; - line-height: 1; - cursor: pointer; - color: var(--color-ink-3, #64748b); -} - -.aksrt-forum-report-modal__target { - margin: 0 0 14px; - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-report-modal__actions { - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-forum-report-modal__feedback { - font-size: 13px; -} - -.is-report-modal-open { - overflow: hidden; -} - -.aksrt-forum-reply__report-btn { - display: inline-flex; - align-items: center; - gap: 4px; - border: none; - background: transparent; - color: var(--color-ink-4, #94a3b8); - cursor: pointer; - padding: 0; - font-size: 12px; -} - -.aksrt-forum-reply__report-btn:hover { - color: var(--color-error, #ef4444); -} - -/* ── moderation enhancements (§7.5) ── */ - -.aksrt-forum-topic__moderation-note { - margin-bottom: 16px; - padding: 12px 14px; - border-radius: 10px; - background: var(--surface-2, #f8fafc); - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-topic__moderation-note strong { - color: var(--color-ink, #334155); - margin-right: 6px; -} - -.aksrt-forum-topic__field small { - display: block; - margin-top: 6px; - color: var(--color-ink-4, #94a3b8); - font-size: 12px; -} - -/* ── paywall / credits unlock (§7.3) ── */ - -.aksrt-forum-topic__pay-price { - display: inline-block; - margin-top: 12px; - padding: 6px 16px; - background: #fff7e6; - color: #d46b08; - border-radius: 8px; - font-size: 15px; - font-weight: 600; -} - -.aksrt-forum-topic__unlock-feedback { - display: block; - margin-top: 10px; - font-size: 13px; -} - -.aksrt-forum-compose__visibility-extra select[multiple] { - min-height: 90px; -} - -.aksrt-forum-compose__visibility-extra input[type="number"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - font-family: inherit; -} - -/* ── adopted answer (§7.3) ── */ - -.aksrt-forum-reply.is-adopted { - border: 2px solid var(--color-success, #52c41a); - border-radius: var(--radius-m, 8px); - padding: 12px; - background: #f6ffed; -} - -.aksrt-forum-reply__adopted-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - background: var(--color-success, #52c41a); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; -} - -.aksrt-forum-reply__adopt-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border: 1px solid var(--color-success, #52c41a); - border-radius: var(--radius-s, 4px); - background: transparent; - color: var(--color-success, #52c41a); - font-size: 12px; - cursor: pointer; - transition: all .15s; -} - -.aksrt-forum-reply__adopt-btn:hover:not(:disabled) { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-reply__adopt-btn:disabled, -.aksrt-forum-reply__adopt-btn.is-adopted { - opacity: .6; - cursor: not-allowed; -} - -/* ── moderator role badge (§7.5) ── */ - -.aksrt-forum-reply__role-badge { - display: inline-flex; - align-items: center; - padding: 1px 6px; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - line-height: 1.5; -} - -.aksrt-forum-reply__role-badge--admin { - background: #fff1f0; - color: #cf1322; -} - -.aksrt-forum-reply__role-badge--mod { - background: #e6f7ff; - color: #096dd9; -} - -/* ── §7.6 forum comment engine ── */ - diff --git a/assets/css/frontend/forum/extras/like-button.css b/assets/css/frontend/forum/extras/like-button.css deleted file mode 100644 index fd7099ce..00000000 --- a/assets/css/frontend/forum/extras/like-button.css +++ /dev/null @@ -1,62 +0,0 @@ -/* --- like button --- */ - -.aksrt-forum-reply__like-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border: none; - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: color .2s; -} - -.aksrt-forum-reply__like-btn:hover { - color: var(--color-primary); -} - -.aksrt-forum-reply__like-btn.is-active { - color: var(--color-primary); -} - -.aksrt-forum-reply__like-count { - font-size: 12px; - min-width: 10px; -} - -/* --- nested reply (楼中楼) --- */ - -.aksrt-forum-reply--nested { - padding-left: 0; - margin-left: 0; -} - -.aksrt-forum-reply__children { - margin-top: 8px; - padding: 8px 0 0 12px; - border-left: 2px solid var(--c-border, #f0f0f0); -} - -.aksrt-forum-reply__children .aksrt-forum-reply { - padding: 8px 0; - margin-bottom: 0; - border-bottom: 1px solid var(--c-border-light, #f5f5f5); -} - -.aksrt-forum-reply__children .aksrt-forum-reply:last-child { - border-bottom: none; -} - -.aksrt-forum-reply__avatar--sm img { - width: 28px; - height: 28px; - border-radius: 50%; -} - -.aksrt-forum-reply__reply-to { - font-size: 12px; - color: var(--c-text-3, #999); -} - diff --git a/assets/css/frontend/forum/extras/load-more.css b/assets/css/frontend/forum/extras/load-more.css deleted file mode 100644 index 9a201bc9..00000000 --- a/assets/css/frontend/forum/extras/load-more.css +++ /dev/null @@ -1,23 +0,0 @@ -/* --- load more --- */ - -.aksrt-forum-replies__load-more { - text-align: center; - padding: 16px 0; -} - -.aksrt-forum-replies__more-btn { - padding: 8px 24px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius, 6px); - background: none; - color: var(--c-text-2, #666); - font-size: 14px; - cursor: pointer; - transition: all .2s; -} - -.aksrt-forum-replies__more-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - diff --git a/assets/css/frontend/forum/extras/quick-post-bar.css b/assets/css/frontend/forum/extras/quick-post-bar.css deleted file mode 100644 index 8e151fe1..00000000 --- a/assets/css/frontend/forum/extras/quick-post-bar.css +++ /dev/null @@ -1,31 +0,0 @@ -/* Quick post bar */ - -.aksrt-forum-quick-post { - margin-bottom: 16px; -} - -.aksrt-forum-quick-post__input { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 16px; - border: 1px solid var(--c-border, #e8e8e8); - border-radius: var(--radius, 6px); - background: var(--c-fill-1, #fafafa); - color: var(--c-text-3, #999); - font-size: 14px; - text-decoration: none; - transition: border-color .2s, box-shadow .2s; -} - -.aksrt-forum-quick-post__input:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-forum-quick-post__input img { - width: 32px; - height: 32px; - border-radius: 50%; -} - diff --git a/assets/css/frontend/forum/extras/reply-action-buttons.css b/assets/css/frontend/forum/extras/reply-action-buttons.css deleted file mode 100644 index 7118ef22..00000000 --- a/assets/css/frontend/forum/extras/reply-action-buttons.css +++ /dev/null @@ -1,110 +0,0 @@ -/* --- reply action buttons --- */ - -.aksrt-forum-reply__reply-btn, -.aksrt-forum-reply__top-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border: none; - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: color .2s; -} - -.aksrt-forum-reply__reply-btn:hover, -.aksrt-forum-reply__top-btn:hover { - color: var(--color-primary); -} - -/* ── §7.3 Hidden content block [aksrt_hide] ── */ - -.aksrt-forum-hide { - margin: 16px 0; -} - -.aksrt-forum-hide--locked { - padding: 16px; - border: 1px dashed var(--c-border, #d9d9d9); - border-radius: var(--radius, 6px); - background: var(--c-fill-1, #fafafa); - text-align: center; -} - -.aksrt-forum-hide__message { - font-size: 13px; - color: var(--c-text-3, #999); -} - -.aksrt-forum-hide__message i { - margin-right: 4px; - color: var(--c-warning, #faad14); -} - -.aksrt-forum-hide--revealed { - padding: 12px 16px; - border-left: 3px solid var(--c-success, #52c41a); - background: var(--c-fill-1, #f6ffed); - border-radius: 0 var(--radius, 6px) var(--radius, 6px) 0; -} - -/* ── §7.10 Forum home tab engine ── */ - -.aksrt-forum-home-tabs { - display: flex; - gap: 4px; - padding: 0 0 12px; - margin-bottom: 16px; - border-bottom: 1px solid var(--c-border, #e8e8e8); - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.aksrt-forum-home-tabs__btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 6px 16px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius-pill, 999px); - background: none; - color: var(--c-text-3, #999); - font-size: 14px; - font-weight: 500; - cursor: pointer; - white-space: nowrap; - transition: all .2s; -} - -.aksrt-forum-home-tabs__btn:hover { - color: var(--c-text-1, #333); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-home-tabs__btn.is-active { - background: var(--color-primary); - color: #fff; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-home-tabs__badge { - display: inline-block; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: 9px; - background: #ff4d4f; - color: #fff; - font-size: 11px; - font-weight: 700; - line-height: 18px; - text-align: center; -} - -.aksrt-forum-home__load-more { - text-align: center; - padding: 16px 0; -} - diff --git a/assets/css/frontend/forum/extras/reply-header-tabs-filters.css b/assets/css/frontend/forum/extras/reply-header-tabs-filters.css deleted file mode 100644 index 5bb6e20f..00000000 --- a/assets/css/frontend/forum/extras/reply-header-tabs-filters.css +++ /dev/null @@ -1,69 +0,0 @@ -/* --- reply header tabs & filters --- */ - -.aksrt-forum-replies__header { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid var(--c-border, #e8e8e8); -} - -.aksrt-forum-replies__tabs { - display: flex; - align-items: center; - gap: 4px; -} - -.aksrt-forum-replies__tab { - padding: 4px 12px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius-pill, 999px); - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: all .2s; -} - -.aksrt-forum-replies__tab:hover { - color: var(--c-text-1, #333); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-replies__tab.is-active { - background: var(--color-primary); - color: #fff; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-replies__filter { - display: flex; - align-items: center; - gap: 4px; - font-size: 13px; - color: var(--c-text-3, #999); - cursor: pointer; - margin-left: 8px; -} - -.aksrt-forum-replies__filter input[type="checkbox"] { - width: 14px; - height: 14px; -} - -.aksrt-forum-replies__empty { - padding: 40px 0; - text-align: center; - color: var(--c-text-3, #bbb); - font-size: 14px; -} - -.aksrt-forum-replies__empty i { - font-size: 24px; - display: block; - margin-bottom: 8px; -} - diff --git a/assets/css/frontend/forum/topic.css b/assets/css/frontend/forum/topic.css deleted file mode 100644 index 82bb2fdd..00000000 --- a/assets/css/frontend/forum/topic.css +++ /dev/null @@ -1,402 +0,0 @@ -/* ============================================================ - * Topic detail page - * ============================================================ */ - -.aksrt-forum-topic { - display: grid; - grid-template-columns: minmax(0, 1fr) 280px; - gap: 24px; - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-topic__shell { - min-width: 0; -} - -.aksrt-forum-topic__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -.aksrt-forum-topic__sidebar-card { - padding: 18px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__sidebar-card h2 { - margin: 0 0 12px; - font-size: 15px; -} - -.aksrt-forum-topic__sidebar-link { - display: flex; - flex-direction: column; - gap: 6px; - color: inherit; - text-decoration: none; -} - -.aksrt-forum-topic__sidebar-link span { - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic__sidebar-author { - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-forum-topic__sidebar-author img { - border-radius: 50%; -} - -.aksrt-forum-topic__sidebar-author strong, -.aksrt-forum-topic__board-link { - color: var(--color-ink); -} - -.aksrt-forum-topic__board-link-wrap { - display: inline-flex; - align-items: center; - gap: 6px; -} - - -.aksrt-forum-topic__breadcrumb { - font-size: 13px; - color: var(--color-ink-4, #8a909c); - margin-bottom: 16px; -} - -.aksrt-forum-topic__breadcrumb a { - color: var(--color-primary); - text-decoration: none; -} - -.aksrt-forum-topic__sep { - margin: 0 6px; -} - -.aksrt-forum-topic__header { - margin-bottom: 20px; -} - -.aksrt-forum-topic__title { - margin: 0 0 8px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.aksrt-forum-topic__badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; -} - -.aksrt-forum-topic__badge--sticky { - background: var(--color-primary); - color: #fff; -} - -.aksrt-forum-topic__badge--sticky-super { - background: var(--focus-color, #fd53a1); - color: #fff; -} - -.aksrt-forum-topic__badge--sticky-global { - background: #e53e3e; - color: #fff; -} - -.aksrt-forum-topic__badge--featured { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-topic__badge--locked { - background: #fff7e6; - color: #d46b08; -} - -.aksrt-forum-topic__badge--visibility { - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic__meta { - display: flex; - flex-wrap: wrap; - gap: 16px; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__author { - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic__author img { - border-radius: 50%; -} - -.aksrt-forum-topic__quick-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 14px; -} - -.aksrt-forum-topic__quick-action, -.aksrt-forum-topic__primary-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 16px; - border: none; - border-radius: 999px; - background: var(--surface-1, #fff); - color: var(--color-ink); - text-decoration: none; - cursor: pointer; -} - -.aksrt-forum-topic__primary-btn { - background: var(--color-primary); - color: #fff; -} - -.aksrt-forum-topic__taxonomy { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 20px; -} - -.aksrt-forum-topic__chip { - display: inline-flex; - align-items: center; - padding: 6px 10px; - border-radius: 999px; - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); - text-decoration: none; - font-size: 13px; -} - -.aksrt-forum-topic__chip--special { - background: #fff1f0; - color: #cf1322; -} - -.aksrt-forum-topic__content { - padding: 22px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); - margin-bottom: 24px; - line-height: 1.8; - font-size: 15px; -} - -.aksrt-forum-topic__content img { - max-width: 100%; - border-radius: 8px; -} - -.aksrt-forum-topic__vote, -.aksrt-forum-topic__moderation { - margin-bottom: 24px; - padding: 20px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__vote-head, -.aksrt-forum-topic__moderation-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 16px; -} - -.aksrt-forum-topic__vote-head h2, -.aksrt-forum-topic__moderation-head h2 { - margin: 0; - font-size: 18px; -} - -.aksrt-forum-topic__vote-head span, -.aksrt-forum-topic__moderation-head p { - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-topic__vote-options { - display: grid; - gap: 10px; -} - -.aksrt-forum-topic__vote-option { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 10px; - padding: 12px 14px; - border-radius: 10px; - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-topic__vote-option.is-selected { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 12%, transparent)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-forum-topic__vote-count { - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__vote-actions, -.aksrt-forum-topic__moderation-actions { - display: flex; - align-items: center; - gap: 12px; - margin-top: 16px; -} - -.aksrt-forum-topic__moderation-grid, -.aksrt-forum-compose__grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; -} - -.aksrt-forum-topic__field select, -.aksrt-forum-topic__field textarea, -.aksrt-forum-compose__field input[type="text"], -.aksrt-forum-compose__field textarea, -.aksrt-forum-compose__field select { - width: 100%; -} - -.aksrt-forum-topic__field label { - display: block; - margin-bottom: 6px; - font-size: 14px; - font-weight: 500; -} - -.aksrt-forum-topic__field select, -.aksrt-forum-topic__field textarea { - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-family: inherit; - font-size: 14px; -} - -.aksrt-forum-topic__denied { - padding: 56px 24px; - text-align: center; - background: var(--surface-1, #fff); - border-radius: 12px; -} - -.aksrt-forum-topic__denied i { - font-size: 42px; - margin-bottom: 14px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__replies { - margin-bottom: 24px; -} - -.aksrt-forum-topic__replies-title { - font-size: 16px; - font-weight: 600; - margin-bottom: 16px; - color: var(--color-ink); -} - -.aksrt-forum-topic__reply-form { - padding: 20px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__reply-form h2, -.aksrt-forum-topic__reply-form h3 { - margin: 0 0 12px; - font-size: 15px; - color: var(--color-ink); -} - -.aksrt-forum-topic__reply-field textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 12px; - font-size: 14px; - resize: vertical; - font-family: inherit; -} - -.aksrt-forum-topic__reply-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 20px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; - font-size: 14px; -} - -.aksrt-forum-topic__reply-btn:hover { - opacity: 0.85; -} - -.aksrt-forum-topic__locked-notice, -.aksrt-forum-topic__login-notice { - text-align: center; - padding: 20px; - color: var(--color-ink-3, #8a909c); - font-size: 14px; -} - -@media (max-width: 960px) { - .aksrt-forum-topic { - grid-template-columns: minmax(0, 1fr); - } -} - -.aksrt-forum-topic__login-notice a { - color: var(--color-primary); -} - diff --git a/assets/css/frontend/pages/board-cards.css b/assets/css/frontend/pages/board-cards.css deleted file mode 100644 index 93fc53c6..00000000 --- a/assets/css/frontend/pages/board-cards.css +++ /dev/null @@ -1,58 +0,0 @@ -/* Board cards */ -.aksrt-forum-boards__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 12px; - margin-bottom: 32px; -} - -.aksrt-forum-board-card { - display: flex; - gap: 12px; - padding: 16px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line, #e8e8e8); - border-radius: 8px; - transition: box-shadow 0.2s; -} - -.aksrt-forum-board-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); -} - -.aksrt-forum-board-card__icon { - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: var(--color-primary-soft, #e6f4ff); - color: var(--color-primary); - font-size: 20px; - flex-shrink: 0; -} - -.aksrt-forum-board-card__title { - margin: 0 0 4px; - font-size: 14px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-forum-board-card__desc { - margin: 0 0 8px; - font-size: 12px; - color: var(--color-ink-3, #999); - line-height: 1.5; -} - -.aksrt-forum-board-card__meta { - font-size: 11px; - color: var(--color-ink-3, #999); -} - -.aksrt-forum-board-card__meta i { - margin-right: 3px; -} - diff --git a/assets/css/frontend/pages/topic-rows.css b/assets/css/frontend/pages/topic-rows.css deleted file mode 100644 index 6aeafc37..00000000 --- a/assets/css/frontend/pages/topic-rows.css +++ /dev/null @@ -1,56 +0,0 @@ -/* Topic rows */ -.aksrt-forum-topics__list { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 32px; -} - -.aksrt-forum-topic-row { - display: flex; - gap: 12px; - padding: 12px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line, #e8e8e8); - border-radius: 8px; - transition: border-color 0.2s; -} - -.aksrt-forum-topic-row:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic-row__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; -} - -.aksrt-forum-topic-row__title { - margin: 0 0 4px; - font-size: 14px; - font-weight: 600; -} - -.aksrt-forum-topic-row__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-row__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-row__meta { - display: flex; - gap: 12px; - font-size: 11px; - color: var(--color-ink-3, #999); -} - -.aksrt-forum-topic-row__meta i { - margin-right: 3px; -} - diff --git a/assets/css/frontend/product-archive.css b/assets/css/frontend/product-archive.css deleted file mode 100644 index 8ae89bc3..00000000 --- a/assets/css/frontend/product-archive.css +++ /dev/null @@ -1,755 +0,0 @@ -/** - * AKSRT Product Archive — 商品归档页样式 - * - * 商品分类页、标签页共用样式。 - * 卡片网格、价格徽章、空状态。 - * - * Tokens: 颜色 / 间距 / 圆角 / 阴影 全部来自 core/tokens.css - */ - -/* ================================================ - * 页面头:标题 + 计数 - * ================================================ */ -.aksrt-product-archive { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-product-archive__header { - margin-bottom: var(--space-6); - padding: var(--space-5); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product-archive__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; -} - -.aksrt-product-archive__title i { - color: var(--color-primary, var(--color-primary)); -} - -.aksrt-product-archive__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3); - line-height: 1.6; -} - -.aksrt-product-archive__count { - margin: 0; - font-size: 13px; - color: var(--color-ink-4, #8A8F98); -} - - -.aksrt-product-archive__eyebrow { - display: inline-flex; - align-items: center; - min-height: 28px; - padding: 0 12px; - margin-bottom: 10px; - border-radius: 999px; - background: var(--surface-2, #f4f5f7); - color: var(--color-ink-3); - font-size: 12px; - font-weight: 600; -} -.aksrt-product-archive__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: var(--space-5); - margin-bottom: var(--space-8); -} - -/* ================================================ - * 商品卡片 - * ================================================ */ -.aksrt-product-card { - display: flex; - flex-direction: column; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg, 8px); - overflow: hidden; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); - transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; -} - -.aksrt-product-card:hover { - transform: translateY(-3px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-card__link { - display: flex; - flex-direction: column; - flex: 1; - text-decoration: none; - color: inherit; -} - -/* 商品图片 */ -.aksrt-product-card__image { - position: relative; - aspect-ratio: 4 / 3; - background-color: var(--surface-2, #F4F5F7); - overflow: hidden; -} - -.aksrt-product-card__image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.aksrt-product-card:hover .aksrt-product-card__image img { - transform: scale(1.02); -} - -.aksrt-product-card__image--placeholder { - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ink-4, #8A8F98); - font-size: 32px; -} - -/* 卡片正文 */ -.aksrt-product-card__body { - display: flex; - flex-direction: column; - gap: var(--space-2); - padding: var(--space-4); - flex: 1; -} - -.aksrt-product-card__title { - margin: 0; - font-size: 15px; - font-weight: 600; - line-height: 1.4; - color: var(--color-ink); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.aksrt-product-card__excerpt { - margin: 0; - font-size: 13px; - color: var(--color-ink-3); - line-height: 1.5; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -/* 底部:价格 + 销量 */ -.aksrt-product-card__footer { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-top: auto; - padding-top: var(--space-3); - border-top: 1px solid var(--color-line-soft, #F0F1F3); -} - -.aksrt-product-card__price { - font-size: 18px; - font-weight: 700; - color: var(--color-error, #EF4444); -} - -.aksrt-product-card__sales { - font-size: 12px; - color: var(--color-ink-4, #8A8F98); -} - -/* ================================================ - * 分页 - * ================================================ */ -.aksrt-product-archive__pagination { - display: flex; - justify-content: center; - padding: var(--space-5) 0; -} - -.aksrt-product-archive__pagination .page-numbers { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 36px; - height: 36px; - padding: 0 8px; - margin: 0 4px; - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - background: var(--surface-1, #fff); - color: var(--color-ink-2); - font-size: 14px; - text-decoration: none; - transition: all 0.15s ease; -} - -.aksrt-product-archive__pagination .page-numbers:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary, var(--color-primary)); -} - -.aksrt-product-archive__pagination .page-numbers.current { - background: var(--color-primary, var(--color-primary)); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: #fff; -} - -/* ================================================ - * 空状态 - * ================================================ */ -.aksrt-product-archive__empty { - text-align: center; - padding: var(--space-9) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-product-archive__empty i { - font-size: 48px; - color: var(--color-ink-4, #8A8F98); - margin-bottom: var(--space-4); - display: block; -} - -.aksrt-product-archive__empty p { - margin: 0; - font-size: 15px; -} - -/* ================================================ - * 响应式 - * ================================================ */ -@media (max-width: 640px) { - .aksrt-product-archive__grid { - grid-template-columns: repeat(2, 1fr); - gap: var(--space-3); - } - - .aksrt-product-card__body { - padding: var(--space-3); - } - - .aksrt-product-card__title { - font-size: 14px; - } - - .aksrt-product-card__price { - font-size: 16px; - } -} - -@media (max-width: 380px) { - .aksrt-product-archive__grid { - grid-template-columns: 1fr; - } -} - -/* ================================================ - * 产品详情页样式补充 - * ================================================ */ -.aksrt-product { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-product__layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-7); - margin-bottom: var(--space-7); -} - -.aksrt-product__image { - border-radius: var(--radius-lg, 8px); - overflow: hidden; - background: var(--surface-2, #F4F5F7); -} - -.aksrt-product__img { - width: 100%; - display: block; -} - -.aksrt-product__img-placeholder { - aspect-ratio: 1; - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ink-4, #8A8F98); - font-size: 64px; -} - -.aksrt-product__info { - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.aksrt-product__title { - margin: 0; - font-size: 24px; - font-weight: 700; - color: var(--color-ink); - line-height: 1.3; -} - -.aksrt-product__price-row { - display: flex; - align-items: baseline; - gap: var(--space-3); -} - -.aksrt-product__price { - font-size: 28px; - font-weight: 700; - color: var(--color-error, #EF4444); -} - -.aksrt-product__sales { - font-size: 13px; - color: var(--color-ink-4, #8A8F98); -} - -.aksrt-product__excerpt { - margin: 0; - font-size: 14px; - color: var(--color-ink-3); - line-height: 1.6; -} - -.aksrt-product__meta { - font-size: 14px; - color: var(--color-ink-2); -} - -.aksrt-product__meta i { - margin-right: 4px; -} - -.aksrt-product__actions { - margin-top: var(--space-4); -} - -.aksrt-product__buy-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 32px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-product__buy-btn:hover { - opacity: 0.85; -} - -.aksrt-product__detail { - padding: var(--space-6); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product__detail-title { - margin: 0 0 var(--space-4); - font-size: 18px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-product__detail-content { - font-size: 15px; - line-height: 1.8; - color: var(--color-ink-2); -} - -.aksrt-product__detail-content img { - max-width: 100%; - border-radius: var(--radius, 10px); -} - -.aksrt-product__empty { - text-align: center; - padding: var(--space-10) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-product__empty i { - display: block; - margin-bottom: var(--space-4); -} - -.aksrt-product__empty p { - margin: 0; - font-size: 16px; -} - -/* 产品详情页响应式 */ -@media (max-width: 767px) { - .aksrt-product__layout { - grid-template-columns: 1fr; - gap: var(--space-5); - } - - .aksrt-product__title { - font-size: 20px; - } - - .aksrt-product__price { - font-size: 24px; - } -} - -/* ================================================ - * 购物车页样式 - * ================================================ */ -.aksrt-cart { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-cart__title { - margin: 0 0 var(--space-6); - font-size: 24px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-cart__layout { - display: grid; - grid-template-columns: 1fr 320px; - gap: var(--space-6); -} - -.aksrt-cart__items { - min-height: 200px; -} - -.aksrt-cart__item { - display: flex; - align-items: center; - gap: var(--space-4); - padding: var(--space-4); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - margin-bottom: var(--space-3); -} - -.aksrt-cart__item-img { - width: 80px; - height: 80px; - object-fit: cover; - border-radius: var(--radius-sm, 6px); -} - -.aksrt-cart__item-info { - flex: 1; - min-width: 0; -} - -.aksrt-cart__item-info h4 { - margin: 0 0 4px; - font-size: 15px; - font-weight: 500; - color: var(--color-ink); -} - -.aksrt-cart__item-price { - font-size: 14px; - color: var(--color-ink-3); -} - -.aksrt-cart__item-qty { - display: flex; - align-items: center; - gap: var(--space-2); -} - -.aksrt-cart__qty-btn { - width: 28px; - height: 28px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm, 6px); - background: var(--surface-1, #fff); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -.aksrt-cart__qty-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-cart__item-subtotal { - font-weight: 600; - color: var(--color-error, #EF4444); - min-width: 80px; - text-align: right; -} - -.aksrt-cart__remove-btn { - width: 32px; - height: 32px; - border: none; - background: none; - cursor: pointer; - color: var(--color-ink-4, #8A8F98); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm, 6px); -} - -.aksrt-cart__remove-btn:hover { - background: var(--surface-2, #F4F5F7); - color: var(--color-error, #EF4444); -} - -.aksrt-cart__sidebar { - position: sticky; - top: 24px; -} - -.aksrt-cart__summary { - padding: var(--space-5); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg, 8px); -} - -.aksrt-cart__summary-title { - margin: 0 0 var(--space-4); - font-size: 16px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-cart__summary-row { - display: flex; - justify-content: space-between; - margin-bottom: var(--space-3); - font-size: 14px; - color: var(--color-ink-2); -} - -.aksrt-cart__summary-total { - display: flex; - justify-content: space-between; - padding-top: var(--space-3); - border-top: 1px solid var(--color-line); - margin-bottom: var(--space-5); - font-weight: 600; -} - -.aksrt-cart__total-price { - font-size: 20px; - color: var(--color-error, #EF4444); -} - -.aksrt-cart__checkout-btn { - width: 100%; - padding: 12px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-cart__checkout-btn:hover { - opacity: 0.85; -} - -.aksrt-cart__checkout-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.aksrt-cart__empty { - text-align: center; - padding: var(--space-9) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-cart__empty i { - display: block; - margin-bottom: var(--space-4); -} - -.aksrt-cart__empty p { - margin: 0 0 var(--space-4); - font-size: 15px; -} - -/* 购物车响应式 */ -@media (max-width: 767px) { - .aksrt-cart__layout { - grid-template-columns: 1fr; - } - - .aksrt-cart__sidebar { - position: static; - } -} - -/* ================================================ - * 商品编辑页样式 - * ================================================ */ -.aksrt-product-edit { - padding-top: var(--space-6); - padding-bottom: var(--space-9); - max-width: 720px; - margin: 0 auto; -} - -.aksrt-product-edit__title { - margin: 0 0 var(--space-6); - font-size: 24px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-product-edit__form { - padding: var(--space-6); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product-edit__field { - margin-bottom: var(--space-5); -} - -.aksrt-product-edit__field label { - display: block; - font-size: 14px; - font-weight: 500; - color: var(--color-ink); - margin-bottom: var(--space-2); -} - -.aksrt-product-edit__field input, -.aksrt-product-edit__field textarea { - width: 100%; - padding: 10px 14px; - font-size: 14px; - color: var(--color-ink); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - transition: border-color 0.15s; -} - -.aksrt-product-edit__field input:focus, -.aksrt-product-edit__field textarea:focus { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-product-edit__field textarea { - resize: vertical; - min-height: 100px; -} - -.aksrt-product-edit__actions { - display: flex; - gap: var(--space-3); - margin-top: var(--space-6); -} - -.aksrt-product-edit__actions .btn-primary { - padding: 10px 24px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 15px; - font-weight: 500; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-product-edit__actions .btn-primary:hover { - opacity: 0.85; -} - -.aksrt-product-edit__actions .btn-ghost { - padding: 10px 24px; - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - background: var(--surface-1, #fff); - color: var(--color-ink-2); - font-size: 15px; - text-decoration: none; - transition: border-color 0.15s; -} - -.aksrt-product-edit__actions .btn-ghost:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px} -.aksrt-product-archive__sort{display:inline-flex;padding:8px 14px;border-radius:999px;background:var(--surface-1,#fff);color:var(--color-ink-3,#5c6370);text-decoration:none;font-weight:600;font-size:14px;box-shadow:var(--shadow-sm)} -.aksrt-product-archive__sort.is-active,.aksrt-product-archive__sort:hover{background:var(--color-primary,#f04494);color:#fff} -.aksrt-product-archive__load-more{display:flex;justify-content:center;margin:20px 0} -.aksrt-revenue-page__layout{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:20px 0} -.aksrt-revenue-page__panel{padding:18px;border-radius:16px;background:var(--surface-1,#fff);box-shadow:var(--shadow-sm)} -.aksrt-revenue-page__withdraw-form label{display:flex;flex-direction:column;gap:6px;margin-bottom:12px} -.aksrt-revenue-page__withdraw-form input,.aksrt-revenue-page__withdraw-form select,.aksrt-revenue-page__withdraw-form textarea{width:100%;padding:10px 12px;border:1px solid var(--color-line-soft,rgba(0,0,0,.08));border-radius:10px} -.aksrt-revenue-page__item{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-line-soft,rgba(0,0,0,.06))} -.aksrt-revenue-page__item .is-in{color:#27ae60;font-weight:700} -.aksrt-revenue-page__item .is-out{color:#e74c3c;font-weight:700} -.aksrt-product-editor__hint{font-size:13px;color:var(--color-ink-4,#8a909c);margin:0 0 10px} - -/* ===== zibll-parity cat filter / skeleton ===== */ -.aksrt-shop-cat-filter.shop-cat-filter{margin-top:14px} -.shop-cat-filter-tab{display:flex;gap:10px;overflow-x:auto;list-style:none;margin:0 0 10px;padding:0 0 4px;-webkit-overflow-scrolling:touch} -.shop-cat-filter-tab li{flex:0 0 auto} -.shop-cat-filter-tab a, -.shop-cat-filter-tab span, -.shop-cat-filter-child a, -.shop-cat-filter-child span{display:inline-flex;align-items:center;padding:6px 12px;border-radius:999px;font-size:13px;font-weight:600;text-decoration:none;color:var(--color-ink-2,#5c6370);background:var(--surface-4,#f3f4f6)} -.shop-cat-filter-tab .active span, -.shop-cat-filter-child .focus-color, -.shop-cat-filter-orderby .focus-color{color:var(--color-primary,#f04494);background:color-mix(in srgb,var(--color-primary,#f04494) 12%, #fff)} -.shop-cat-filter-child{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px} -.shop-cat-filter-orderby{gap:8px} -.shop-cat-filter-orderby .opacity5{opacity:.55;margin-right:4px;font-size:13px} -.aksrt-product-archive__skeleton{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;padding:8px 0 16px} -.aksrt-product-card--placeholder{min-height:220px;border-radius:12px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 37%,#f3f4f6 63%);background-size:400% 100%;animation:aksrtShopSk 1.2s ease infinite} -@keyframes aksrtShopSk{0%{background-position:100% 0}100%{background-position:0 0}} -@media (max-width:992px){.aksrt-product-archive__skeleton{grid-template-columns:repeat(2,minmax(0,1fr))}} -@media (max-width:560px){.aksrt-product-archive__skeleton{grid-template-columns:1fr}} diff --git a/assets/css/routes/cart.css b/assets/css/routes/cart.css deleted file mode 100644 index c7035b0e..00000000 --- a/assets/css/routes/cart.css +++ /dev/null @@ -1,400 +0,0 @@ -.aksrt-cart-page { - padding: 28px 0 56px; -} - -.aksrt-cart-page__header, -.aksrt-cart-page__panel-head, -.aksrt-cart-page__summary-total, -.aksrt-cart-page__coupon-row, -.aksrt-cart-page__item, -.aksrt-cart-page__item-qty, -.aksrt-cart-page__option-columns { - display: flex; -} - -.aksrt-cart-page__header, -.aksrt-cart-page__panel-head, -.aksrt-cart-page__summary-total, -.aksrt-cart-page__coupon-row, -.aksrt-cart-page__option-columns { - justify-content: space-between; - gap: 16px; -} - -.aksrt-cart-page__layout { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 24px; - align-items: start; -} - -.aksrt-cart-page__title { - margin: 0 0 8px; - font-size: 30px; - line-height: 1.1; -} - -.aksrt-cart-page__subtitle, -.aksrt-cart-page__hint, -.aksrt-cart-page__option-body span { - color: var(--color-ink-3, #5d6676); -} - -.aksrt-cart-page__pending { - min-width: 280px; -} - -.aksrt-cart-page__pending-list { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; -} - -.aksrt-cart-page__pending-chip { - border: 1px solid var(--color-line, #dde3eb); - border-radius: 999px; - background: var(--surface-1, #fff); - padding: 8px 12px; - font-size: 12px; -} - -.aksrt-cart-page__panel { - background: var(--surface-1, #fff); - border: none; - box-shadow: var(--shadow-sm); - border-radius: var(--radius-2xl, 16px); - padding: 20px; -} - -.aksrt-cart-page__main, -.aksrt-cart-page__grid { - display: grid; - gap: 20px; -} - -.aksrt-cart-page__grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.aksrt-cart-page__items { - display: grid; - gap: 12px; -} - -.aksrt-cart-page__item { - align-items: center; - gap: 14px; - padding: 16px; - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - background: var(--surface-4, #f8fafc); - box-shadow: var(--shadow-sm); -} - -.aksrt-cart-page__item-image { - width: 84px; - height: 84px; - object-fit: cover; - border-radius: 12px; -} - -.aksrt-cart-page__item-image--placeholder { - display: flex; - align-items: center; - justify-content: center; - background: var(--surface-3, #eef2f7); - color: var(--color-ink-4, #8690a0); -} - -.aksrt-cart-page__item-main { - flex: 1; -} - -.aksrt-cart-page__item-main h3, -.aksrt-cart-page__mini-title, -.aksrt-cart-page__panel h2 { - margin: 0 0 8px; -} - -.aksrt-cart-page__item-qty { - align-items: center; - gap: 10px; -} - -.aksrt-cart-page__item-qty button, -.aksrt-cart-page__item-remove { - width: 34px; - height: 34px; - border: 1px solid var(--color-line, #dce3ec); - border-radius: 10px; - background: #fff; -} - -.aksrt-cart-page__item-total { - min-width: 92px; - text-align: right; -} - -.aksrt-cart-page__option-list { - display: grid; - gap: 12px; -} - -.aksrt-cart-page__option-card { - display: flex; - gap: 10px; - padding: 14px; - border: 1px solid var(--color-line, #dde3eb); - border-radius: var(--radius-lg); - background: var(--surface-4, #fff); -} - -.aksrt-cart-page__option-body { - display: grid; - gap: 4px; -} - -.aksrt-cart-page__field { - display: grid; - gap: 8px; -} - -.aksrt-cart-page__input, -.aksrt-cart-page__textarea { - width: 100%; - border: 1px solid var(--color-line, #dce3ec); - border-radius: 12px; - padding: 11px 14px; - background: #fff; -} - -.aksrt-cart-page__summary-list { - display: grid; - gap: 12px; - margin: 0 0 20px; -} - -.aksrt-cart-page__summary-list div, -.aksrt-cart-page__summary-total { - align-items: center; -} - -.aksrt-cart-page__summary-list div { - display: flex; - justify-content: space-between; -} - -.aksrt-cart-page__summary-total { - padding-top: 16px; - margin-bottom: 18px; - border-top: 1px solid var(--color-line, #e7ebf1); -} - -.aksrt-cart-page__summary-total strong { - font-size: 28px; -} - -.aksrt-cart-page__submit { - width: 100%; -} - -.aksrt-cart-page__empty { - text-align: center; - padding: 44px 20px; -} - -.aksrt-cart-page__empty i { - display: block; - margin-bottom: 12px; - font-size: 42px; - color: var(--color-ink-4, #8690a0); -} - -@media (max-width: 980px) { - .aksrt-cart-page__layout, - .aksrt-cart-page__grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 767px) { - .aksrt-cart-page__title { - font-size: 22px; - } - - .aksrt-cart-page__option-columns { - flex-direction: column; - } - - .aksrt-cart-page__field-row { - grid-template-columns: 1fr; - } - - .aksrt-cart-page__pending { - min-width: 0; - } - - .aksrt-modal__dialog { - width: 95%; - max-height: 90vh; - } - - .aksrt-modal__body { - padding: 16px; - } -} - -@media (max-width: 480px) { - .aksrt-cart-page__header { - flex-direction: column; - align-items: flex-start; - } - - .aksrt-cart-page__summary { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 100; - border-radius: 16px 16px 0 0; - box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1); - } - - .aksrt-cart-page__submit { - width: 100%; - } -} - -/* ── Address Card Actions ── */ - -.aksrt-cart-page__address-card { - flex-direction: column; - gap: 8px; -} - -.aksrt-cart-page__address-label { - display: flex; - gap: 10px; - flex: 1; - cursor: pointer; -} - -.aksrt-cart-page__address-actions { - display: flex; - gap: 8px; - padding-left: 28px; -} - -.aksrt-cart-page__default-tag { - display: inline-block; - font-size: 11px; - padding: 1px 6px; - border-radius: 4px; - background: var(--color-primary-1, #e6f7ff); - color: var(--color-primary, #1890ff); - font-weight: 500; - vertical-align: middle; -} - -.aksrt-cart-page__address-empty { - text-align: center; - padding: 32px 16px; - color: var(--color-ink-4, #8690a0); -} - -.aksrt-cart-page__address-empty i { - font-size: 28px; - margin-bottom: 8px; - display: block; -} - -.aksrt-cart-page__field-row { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; -} - -.aksrt-cart-page__checkbox { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 14px; -} - -/* ── Shared Modal ── */ - -.aksrt-modal { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; -} - -.aksrt-modal__overlay { - position: absolute; - inset: 0; - background: rgba(18, 18, 18, 0.48); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.aksrt-modal__dialog { - position: relative; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - width: 90%; - max-width: 520px; - max-height: 85vh; - overflow-y: auto; - box-shadow: var(--shadow-drawer); -} - -.aksrt-modal__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 24px; - border-bottom: 1px solid var(--color-line, #e3e8ef); -} - -.aksrt-modal__header h3 { - margin: 0; - font-size: 18px; -} - -.aksrt-modal__close { - border: none; - background: none; - font-size: 24px; - cursor: pointer; - color: var(--color-ink-3, #5d6676); - padding: 0; - line-height: 1; -} - -.aksrt-modal__body { - padding: 20px 24px; - display: grid; - gap: 16px; -} - -.aksrt-modal__footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding-top: 8px; -} - -/* Soft focus ring (#77 recipe) — avoid hard primary stroke */ -.aksrt-cart-page__input:focus, -.aksrt-cart-page__textarea:focus, -.aksrt-cart-page__qty-btn:focus-visible { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} diff --git a/assets/css/routes/product.css b/assets/css/routes/product.css deleted file mode 100644 index 153f5094..00000000 --- a/assets/css/routes/product.css +++ /dev/null @@ -1,491 +0,0 @@ -/** - * AKSRT Product Page — zibll-aligned - * - * 包含:商品详情页 + 前台商品编辑器 - */ - -.aksrt-product-page, -.aksrt-product-editor { - padding: 28px 0 56px; -} - -.aksrt-product-page__layout, -.aksrt-product-page__detail-grid, -.aksrt-product-editor__layout, -.aksrt-product-editor__columns, -.aksrt-product-page__details, -.aksrt-product-page__actions { - display: grid; - gap: 20px; -} - -.aksrt-product-page__layout, -.aksrt-product-editor__layout { - grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); - align-items: start; -} - -.aksrt-product-page__detail-grid { - grid-template-columns: minmax(0, 1fr) 320px; - margin-top: 24px; -} - -/* ===== Panel surfaces ===== */ -.aksrt-product-page__panel, -.aksrt-product-editor__panel { - background: var(--surface-1); - border: none; - box-shadow: var(--shadow-sm); - border-radius: var(--radius-2xl, 16px); - padding: 20px; -} - -.aksrt-product-page__title, -.aksrt-product-editor__title { - margin: 0 0 10px; - font-size: 30px; - line-height: 1.1; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-product-page__gallery, -.aksrt-product-editor__main, -.aksrt-product-editor__sidebar, -.aksrt-product-page__summary, -.aksrt-product-page__side-panels { - display: grid; - gap: 18px; -} - -/* ===== Gallery ===== */ -.aksrt-product-page__primary-image, -.aksrt-product-page__placeholder { - border-radius: var(--radius-lg); - overflow: hidden; - background: var(--surface-2); -} - -.aksrt-product-page__primary-image img { - display: block; - width: 100%; - aspect-ratio: 1 / 1; - object-fit: cover; -} - -.aksrt-product-page__placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 360px; - font-size: 56px; - color: var(--color-ink-4); -} - -.aksrt-product-page__thumbs { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); - gap: 10px; -} - -.aksrt-product-page__thumb { - border: 1px solid var(--color-line); - border-radius: var(--radius); - padding: 0; - background: var(--surface-1); - overflow: hidden; - cursor: pointer; - transition: border-color var(--dur-fast) var(--ease); -} - -.aksrt-product-page__thumb:hover, -.aksrt-product-page__thumb.is-active { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-page__thumb img { - display: block; - width: 100%; - aspect-ratio: 1 / 1; - object-fit: cover; -} - - -.aksrt-product-page__service-strip { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin: 6px 0 4px; -} - -.aksrt-product-page__service-strip span { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 12px; - border-radius: 999px; - background: var(--surface-4); - color: var(--color-ink-3); - font-size: 12px; - font-weight: 500; -} -.aksrt-product-page__price-row, -.aksrt-product-page__quantity, -.aksrt-product-page__term-list, -.aksrt-product-editor__actions { - display: flex; - gap: 10px; - flex-wrap: wrap; - align-items: center; -} - -.aksrt-product-page__badge, -.aksrt-product-page__term { - display: inline-flex; - align-items: center; - border-radius: var(--radius-pill); - padding: 6px 12px; - background: var(--surface-4); - color: var(--color-ink-2); - font-size: 12px; - font-weight: 500; -} - -.aksrt-product-page__badge--promo { - background: rgba(239, 68, 68, 0.12); - color: var(--color-error); -} - -.aksrt-product-page__price { - font-size: 30px; - font-weight: 800; - color: var(--color-error); - line-height: 1; -} - -.aksrt-product-page__sales { - font-size: 13px; - color: var(--color-ink-4); -} - -/* ===== Sticky buybox (zibll-aligned) ===== */ -.aksrt-product-page__summary { - position: sticky; - top: calc(var(--nav-height) + 16px); -} - -/* ===== Meta info blocks ===== */ -.aksrt-product-page__meta, -.aksrt-product-page__info-list { - display: grid; - gap: 12px; - margin: 0; -} - -.aksrt-product-page__meta div, -.aksrt-product-page__detail-block { - padding: 14px; - border: 1px solid var(--color-line); - border-radius: var(--radius-lg); - background: var(--surface-4); -} - -.aksrt-product-page__meta dt { - font-size: 12px; - color: var(--color-ink-4); - margin-bottom: 4px; -} - -.aksrt-product-page__meta dd { - margin: 0; - font-size: 14px; - color: var(--color-ink); - font-weight: 500; -} - -/* ===== Form fields ===== */ -.aksrt-product-page__field, -.aksrt-product-editor__field { - display: grid; - gap: 8px; -} - -.aksrt-product-page__input, -.aksrt-product-editor__input, -.aksrt-product-editor__textarea { - width: 100%; - border: 1px solid var(--color-line); - border-radius: var(--radius); - padding: 11px 14px; - background: var(--surface-1); - font-size: 14px; - color: var(--color-ink); - transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); -} - -.aksrt-product-page__input:focus, -.aksrt-product-editor__input:focus, -.aksrt-product-editor__textarea:focus { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -/* ===== Quantity controls ===== */ -.aksrt-product-page__qty-btn { - width: 36px; - height: 36px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm); - background: var(--surface-1); - color: var(--color-ink-2); - cursor: pointer; - transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); -} - -.aksrt-product-page__qty-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - background: var(--color-primary-softer); -} - -.aksrt-product-page__qty-input { - width: 90px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm); - padding: 8px 12px; - text-align: center; - font-size: 14px; - color: var(--color-ink); -} - -/* ===== Content ===== */ -.aksrt-product-page__content { - line-height: 1.8; - color: var(--color-ink-2); -} - -.aksrt-product-page__content img { - max-width: 100%; - border-radius: var(--radius); -} - -/* ===== Empty state ===== */ -.aksrt-product-page__empty, -.aksrt-product-editor__empty { - text-align: center; - padding: 48px 20px; -} - -.aksrt-product-page__empty i, -.aksrt-product-editor__empty i { - display: block; - font-size: 44px; - color: var(--color-ink-4); - margin-bottom: 12px; -} - -.aksrt-product-editor__checkbox { - display: flex; - gap: 10px; - align-items: center; -} - -.aksrt-product-editor__sidebar { - position: sticky; - top: 24px; -} - -/* ===== Responsive ===== */ -@media (max-width: 980px) { - .aksrt-product-page__layout, - .aksrt-product-page__detail-grid, - .aksrt-product-editor__layout, - .aksrt-product-editor__columns, - .aksrt-product-page__details { - grid-template-columns: 1fr; - } - - .aksrt-product-page__summary { - position: static; - } - - .aksrt-product-editor__sidebar { - position: static; - } -} - -@media (max-width: 767px) { - .aksrt-product-page__title, - .aksrt-product-editor__title { - font-size: 22px; - } - - .aksrt-product-page__thumbs { - gap: 6px; - } - - .aksrt-product-page__spec-values { - flex-wrap: wrap; - } - - .aksrt-product-page__actions { - grid-template-columns: 1fr; - } - - .aksrt-product-page__actions .btn-lg, - .aksrt-product-editor__actions .btn-lg { - width: 100%; - } -} - -@media (max-width: 480px) { - .aksrt-product-page, - .aksrt-product-editor { - padding: 16px 0 40px; - } - - .aksrt-product-page__price { - font-size: 24px; - } - - .aksrt-product-page__meta { - grid-template-columns: 1fr; - } -} - -/* ===== Dark mode ===== */ -html.dark .aksrt-product-page__badge--promo, -html.dark .aksrt-product-page__badge--promo { - background: rgba(239, 68, 68, 0.18); -} - -html.dark .aksrt-product-page__placeholder, -html.dark .aksrt-product-page__placeholder { - background: var(--surface-2); -} - -html.dark .aksrt-product-page__thumb, -html.dark .aksrt-product-page__thumb { - border-color: var(--color-line); -} - -html.dark .aksrt-product-page__content img, -html.dark .aksrt-product-page__content img { - opacity: 0.92; -} - -/* ============================================================ - * SKU 规格选择器 - * ============================================================ */ -.aksrt-product-page__sku-selector { - margin: 16px 0; - padding: 16px; - background: var(--surface-4); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg); -} - -.aksrt-product-page__spec-group { - margin-bottom: 12px; -} - -.aksrt-product-page__spec-group:last-child { - margin-bottom: 0; -} - -.aksrt-product-page__spec-label { - display: block; - font-size: 13px; - font-weight: 600; - color: var(--color-ink-2); - margin-bottom: 8px; -} - -.aksrt-product-page__spec-values { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.aksrt-product-page__spec-btn { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 48px; - height: 32px; - padding: 0 12px; - font-size: 13px; - font-weight: 500; - color: var(--color-ink-2); - background: var(--surface-1); - border: 1px solid var(--color-line); - border-radius: 6px; - cursor: pointer; - transition: all var(--dur-fast) var(--ease); -} - -.aksrt-product-page__spec-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - -.aksrt-product-page__spec-btn.is-active { - background: var(--color-primary-soft); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - font-weight: 600; -} - -.aksrt-product-page__spec-btn:disabled { - opacity: 0.4; - cursor: not-allowed; - text-decoration: line-through; -} - -/* SKU 信息区 */ -.aksrt-product-page__sku-info { - margin-top: 12px; - padding: 10px 12px; - background: var(--surface-1); - border-radius: 6px; - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-product-page__sku-price { - font-size: 18px; - font-weight: 700; - color: var(--color-error); -} - -.aksrt-product-page__sku-stock { - font-size: 13px; - color: var(--color-ink-3); -} - - -/* Product detail shell — align with single content stream */ -.content-shell--product .content-wrap { - padding-top: 8px; -} -.content-shell--product .aksrt-product-page { - width: 100%; -} -.aksrt-product-page__related { - margin-top: 28px; - padding-top: 8px; -} -.aksrt-product-page__related-title { - margin: 0 0 14px; - font-size: 18px; - font-weight: 700; - color: var(--color-ink, #1f2937); -} -.aksrt-product-page__related-grid.aksrt-product-archive__grid { - margin: 0; -} -.aksrt-product-page__merchant-link { - display: inline-block; - margin-top: 4px; -} diff --git a/assets/js/cart-checkout.js b/assets/js/cart-checkout.js deleted file mode 100644 index 80ede97a..00000000 --- a/assets/js/cart-checkout.js +++ /dev/null @@ -1,483 +0,0 @@ -(function () { - "use strict"; - - var page = document.querySelector("[data-aksrt-cart-page]"); - if (!page || !window.aksrtData) { - return; - } - - function restBase() { - return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1"; - } - function rest(path, method, body) { - var opts = { - method: method || "GET", - credentials: "same-origin", - headers: { "X-WP-Nonce": aksrtData.restNonce || "" } - }; - if (body !== undefined) { - opts.headers["Content-Type"] = "application/json"; - opts.body = JSON.stringify(body); - } - return fetch(restBase() + path, opts).then(function (r) { return r.json(); }); - } - function ordersUrl() { - return aksrtData.ordersUrl || (aksrtData.routes && aksrtData.routes.orders) || "/"; - } - function shopHomeUrl() { - return (aksrtData.routes && aksrtData.routes.home) || aksrtData.siteUrl || "/"; - } - var cartItems = []; - var currencySymbol = page.getAttribute("data-currency-symbol") || "¥"; - var itemsEl = page.querySelector("[data-aksrt-cart-items]"); - var subtotalEl = page.querySelector("[data-aksrt-summary-subtotal]"); - var shippingEl = page.querySelector("[data-aksrt-summary-shipping]"); - var discountEl = page.querySelector("[data-aksrt-summary-discount]"); - var totalEl = page.querySelector("[data-aksrt-summary-total]"); - var previewMessageEl = page.querySelector("[data-aksrt-preview-message]"); - var createBtn = page.querySelector("[data-aksrt-create-order]"); - var previewBtn = page.querySelector("[data-aksrt-preview-checkout]"); - - var previewState = null; - var searchParams = new URLSearchParams(window.location.search); - - function getCart() { return cartItems; } - function setCart(items) { cartItems = Array.isArray(items) ? items : []; } - - function money(value) { - return currencySymbol + Number(value || 0).toFixed(2); - } - - function selectedValue(name) { - var checked = page.querySelector('input[name="' + name + '"]:checked'); - return checked ? checked.value : ""; - } - - function getPayload() { - var items = getCart().map(function (item) { - return { - product_id: Number(item.product_id || item.id || 0), - quantity: Number(item.qty || item.quantity || 1), - options_string: item.options_string || "" - }; - }).filter(function (item) { - return item.product_id > 0; - }); - - return { - items: items, - coupon_code: (page.querySelector("[data-aksrt-coupon-code]") || {}).value || "", - shipping_method: selectedValue("aksrt_cart_shipping"), - address_id: Number(selectedValue("aksrt_cart_address") || 0), - payment_method: selectedValue("aksrt_cart_payment"), - customer_note: (page.querySelector("[data-aksrt-customer-note]") || {}).value || "" - }; - } - - function renderCart() { - var items = getCart(); - if (!items.length) { - itemsEl.innerHTML = '' + - '
' + - '' + - "

购物车为空,去商品页添加想购买的内容。

" + - '继续浏览' + - "
"; - subtotalEl.textContent = money(0); - shippingEl.textContent = money(0); - discountEl.textContent = "-" + money(0); - totalEl.textContent = money(0); - createBtn.disabled = true; - previewState = null; - return; - } - - var total = 0; - var html = ""; - items.forEach(function (item, index) { - var qty = Number(item.qty || item.quantity || 1); - var price = Number(item.unit_price != null ? item.unit_price : (item.price != null ? item.price : (item.product && item.product.price) || 0)); - var lineTotal = Number(item.line_total != null ? item.line_total : qty * price); - total += lineTotal; - var title = item.title || (item.product && item.product.title) || "商品"; - var image = item.image || (item.product && item.product.image) || ""; - var opts = item.options_label || item.options_string || ""; - - html += '' + - '
' + - (image ? '' : '
') + - '
' + - '

' + title + "

" + - (opts ? "" + opts + "" : "") + - '

' + money(price) + "

" + - "
" + - '
' + - '' + - "" + qty + "" + - '' + - "
" + - '' + money(lineTotal) + "" + - '' + - "
"; - }); - - itemsEl.innerHTML = html; - subtotalEl.textContent = money(total); - - if (!previewState) { - shippingEl.textContent = money(0); - discountEl.textContent = "-" + money(0); - totalEl.textContent = money(total); - } - } - - function setSummary(data) { - previewState = data || null; - subtotalEl.textContent = money((data && data.subtotal) || 0); - shippingEl.textContent = money((data && data.shipping_fee) || 0); - discountEl.textContent = "-" + money((data && data.discount) || 0); - totalEl.textContent = money((data && data.total) || 0); - createBtn.disabled = !data; - } - - function request(path, body) { return rest(path, "POST", body || {}); } - - function previewCheckout() { - var payload = getPayload(); - if (!payload.items.length) { - previewMessageEl.textContent = "请先添加商品后再试算。"; - return; - } - - previewBtn.disabled = true; - previewMessageEl.textContent = "正在试算订单..."; - - request("/shop/frontend/checkout/preview", payload) - .then(function (result) { - if (!result.success) { - setSummary(null); - previewMessageEl.textContent = result.message || "试算失败。"; - return; - } - - setSummary(result.data); - previewMessageEl.textContent = "已完成试算,可直接创建订单。"; - }) - .catch(function () { - setSummary(null); - previewMessageEl.textContent = "网络错误,请稍后重试。"; - }) - .finally(function () { - previewBtn.disabled = false; - }); - } - - function createOrder() { - var payload = getPayload(); - if (!previewState) { - previewCheckout(); - return; - } - - createBtn.disabled = true; - request("/shop/frontend/orders", payload) - .then(function (result) { - if (!result.success) { - previewMessageEl.textContent = result.message || "订单创建失败。"; - createBtn.disabled = false; - return; - } - - var orderNumber = (result.data && result.data.order_number) || ""; - var nextOrdersUrl = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; - - if (!orderNumber) { - window.location.href = nextOrdersUrl; - return; - } - - // 立即发起支付:跳转型网关直接前往第三方,余额型同步完成。 - request("/shop/frontend/orders/" + orderNumber + "/pay", {}) - .then(function (pay) { - var data = (pay && pay.data) || {}; - if (pay && pay.success && data.redirect && data.payment_url && /^https?:\/\//i.test(data.payment_url)) { - window.location.href = data.payment_url; - return; - } - // 二维码 / 余额 / 失败:统一回订单中心处理(含 ?payment 提示)。 - window.location.href = nextOrdersUrl; - }) - .catch(function () { - window.location.href = nextOrdersUrl; - }); - }) - .catch(function () { - previewMessageEl.textContent = "网络错误,请稍后重试。"; - createBtn.disabled = false; - }); - } - - page.addEventListener("click", function (event) { - var actionButton = event.target.closest("[data-qty-action]"); - if (actionButton) { - var itemEl = actionButton.closest("[data-cart-index]"); - if (!itemEl) return; - var productId = Number(itemEl.getAttribute("data-product-id") || 0); - var optionsString = itemEl.getAttribute("data-options-string") || ""; - var action = actionButton.getAttribute("data-qty-action"); - var path = "/shop/frontend/cart/add"; - var body = { product_id: productId, options_string: optionsString, count: 1 }; - if (action === "dec") { - path = "/shop/frontend/cart/reduce"; - } else if (action === "remove") { - path = "/shop/frontend/cart/remove"; - body = { product_id: productId, options_string: optionsString }; - } else if (action !== "inc") { - return; - } - rest(path, "POST", body).then(function (result) { - if (!result || !result.success) { - window.alert((result && result.message) || "操作失败"); - return; - } - setSummary(null); - loadServerCart(); - }); - return; - } - - if (event.target.closest("[data-aksrt-clear-cart]")) { - rest("/shop/frontend/cart/empty", "POST", {}).then(function () { - setSummary(null); - loadServerCart(); - }); - return; - } - - if (event.target.closest("[data-aksrt-preview-checkout]")) { - previewCheckout(); - return; - } - - if (event.target.closest("[data-aksrt-create-order]")) { - createOrder(); - return; - } - - var resumeButton = event.target.closest("[data-aksrt-resume-order]"); - if (resumeButton) { - window.location.href = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; - } - }); - - function loadServerCart() { - return rest("/shop/frontend/cart", "GET").then(function (result) { - var items = (result && result.success && result.data && result.data.items) ? result.data.items : []; - items = items.map(function (it) { - it.qty = it.quantity || it.qty || 1; - it.price = it.unit_price != null ? it.unit_price : (it.product && it.product.price) || 0; - it.title = it.title || (it.product && it.product.title) || "商品"; - it.image = it.image || (it.product && it.product.image) || ""; - return it; - }); - setCart(items); - renderCart(); - }).catch(function () { - setCart([]); - renderCart(); - }); - } - - var couponInput = page.querySelector("[data-aksrt-coupon-code]"); - if (couponInput && searchParams.has("coupon")) { - couponInput.value = searchParams.get("coupon") || ""; - } - - /* ── Address Book Management ── */ - - var addressModal = page.querySelector("[data-aksrt-address-modal]"); - var addressForm = page.querySelector("[data-aksrt-address-form]"); - var addressList = page.querySelector("[data-aksrt-address-list]"); - var addressTitle = page.querySelector("[data-aksrt-address-modal-title]"); - - function openAddressModal(mode, address) { - if (!addressModal || !addressForm) return; - addressForm.reset(); - addressForm.querySelector('[name="address_id"]').value = ""; - - if (mode === "edit" && address) { - addressTitle.textContent = "编辑地址"; - addressForm.querySelector('[name="address_id"]').value = address.id || ""; - addressForm.querySelector('[name="name"]').value = address.name || ""; - addressForm.querySelector('[name="phone"]').value = address.phone || ""; - addressForm.querySelector('[name="province"]').value = address.province || ""; - addressForm.querySelector('[name="city"]').value = address.city || ""; - addressForm.querySelector('[name="district"]').value = address.district || ""; - addressForm.querySelector('[name="address"]').value = address.address || ""; - addressForm.querySelector('[name="is_default"]').checked = !!address.is_default; - } else { - addressTitle.textContent = "新增地址"; - } - - addressModal.hidden = false; - } - - function closeAddressModal() { - if (addressModal) addressModal.hidden = true; - } - - function refreshAddressList() { - fetch(restBase() + "/shop/frontend/address", { - headers: { "X-WP-Nonce": aksrtData.restNonce } - }) - .then(function (r) { return r.json(); }) - .then(function (result) { - if (!result.success || !addressList) return; - var addresses = (result.data && result.data.addresses) || []; - renderAddressList(addresses); - }) - .catch(function () {}); - } - - function renderAddressList(addresses) { - if (!addressList) return; - if (!addresses.length) { - addressList.innerHTML = - '
' + - '' + - "

暂无收货地址,请先新增。

" + - "
"; - return; - } - - var html = ""; - addresses.forEach(function (addr, index) { - html += - '
' + - '" + - '
' + - '' + - '' + - (addr.is_default ? "" : '') + - "
"; - }); - addressList.innerHTML = html; - } - - function escHtml(str) { - var div = document.createElement("div"); - div.appendChild(document.createTextNode(str || "")); - return div.innerHTML; - } - - function getAddressById(id) { - var cards = addressList ? addressList.querySelectorAll("[data-aksrt-address-card]") : []; - var found = null; - cards.forEach(function (card) { - if (String(card.getAttribute("data-aksrt-address-card")) === String(id)) { - var body = card.querySelector(".aksrt-cart-page__option-body"); - if (body) { - var spans = body.querySelectorAll("span"); - var strong = body.querySelector("strong"); - found = { - id: id, - name: strong ? strong.textContent.replace("默认", "").trim() : "", - phone: spans[0] ? spans[0].textContent : "", - full_text: spans[1] ? spans[1].textContent : "" - }; - } - } - }); - return found; - } - - // Address event delegation - page.addEventListener("click", function (event) { - if (event.target.closest("[data-aksrt-address-add]")) { - openAddressModal("add"); - return; - } - - var editBtn = event.target.closest("[data-aksrt-address-edit]"); - if (editBtn) { - var editId = editBtn.getAttribute("data-aksrt-address-edit"); - var addr = getAddressById(editId); - openAddressModal("edit", addr); - return; - } - - var deleteBtn = event.target.closest("[data-aksrt-address-delete]"); - if (deleteBtn) { - var deleteId = deleteBtn.getAttribute("data-aksrt-address-delete"); - if (!confirm("确定删除该地址吗?")) return; - rest("/shop/frontend/address/" + deleteId, "DELETE").then(function (result) { - if (!result || !result.success) { - alert((result && result.message) || "删除失败。"); - return; - } - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - return; - } - - var defaultBtn = event.target.closest("[data-aksrt-address-set-default]"); - if (defaultBtn) { - var defaultId = defaultBtn.getAttribute("data-aksrt-address-set-default"); - request("/shop/frontend/address/" + defaultId + "/default", {}).then(function (result) { - if (!result.success) { - alert(result.message || "设置失败。"); - return; - } - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - return; - } - - if (event.target.closest("[data-aksrt-address-modal-close]") || event.target.classList.contains("aksrt-modal__overlay")) { - closeAddressModal(); - } - }); - - if (addressForm) { - addressForm.addEventListener("submit", function (event) { - event.preventDefault(); - var formData = new FormData(addressForm); - var addressId = formData.get("address_id"); - var payload = { - name: (formData.get("name") || "").trim(), - phone: (formData.get("phone") || "").trim(), - province: (formData.get("province") || "").trim(), - city: (formData.get("city") || "").trim(), - district: (formData.get("district") || "").trim(), - address: (formData.get("address") || "").trim(), - is_default: formData.get("is_default") === "1" - }; - - if (!payload.name || !payload.phone || !payload.address) { - alert("请填写必填项。"); - return; - } - - var url = addressId - ? "/shop/frontend/address/" + addressId - : "/shop/frontend/address"; - - request(url, payload).then(function (result) { - if (!result.success) { - alert(result.message || "保存失败。"); - return; - } - closeAddressModal(); - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - }); - } - - loadServerCart(); -})(); diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js index 5fea8c08..29b7cf84 100644 --- a/assets/js/cms-download.js +++ b/assets/js/cms-download.js @@ -367,8 +367,6 @@ } function payApiBase(apiKind) { - if (apiKind === 'topic') return '/pay/topic/'; - if (apiKind === 'plate_follow') return '/pay/plate-follow/'; return '/pay/content/'; } @@ -448,34 +446,15 @@ window.location.reload(); return; } - // Content free: reload. Topic/plate free: auto free-order POST (VIP free / free-after-coupon). - if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') { + // Content free: reload. + if (preview.is_free) { window.location.reload(); return; } const isPoints = preview.pay_modo === 'points'; const priceType = (preview._price_type || preview.price_type || ''); - const apiKind = preview._api_kind - || (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content')); - - // Auto-submit free topic/plate orders so user doesn't click through empty cash UI. - if (preview.is_free && (apiKind === 'topic' || apiKind === 'plate_follow') && !isPoints) { - open( - '

' + tt('confirm_buy', '确认购买') + '

' + - '

' + tt('processing', '处理中…') + '

', - returnFocusEl - ); - const freeBtn = document.createElement('button'); - freeBtn.type = 'button'; - freeBtn.style.display = 'none'; - freeBtn.setAttribute('data-price-type', priceType || ''); - document.body.appendChild(freeBtn); - const freeCode = String(preview.coupon_code || preview._coupon_code || ''); - submitPurchase(preview.post_id, 'cash', 'balance', freeBtn, returnFocusEl, freeCode, apiKind); - try { freeBtn.remove(); } catch (e0) { /* ignore */ } - return; - } + const apiKind = preview._api_kind || 'content'; let amountLabel = escapeHtml(preview.amount_label || ''); let baseLabel = escapeHtml(preview.base_label || ''); @@ -697,22 +676,6 @@ return; } - const topicBuyBtn = e.target.closest('[data-aksrt-pay-topic]'); - if (topicBuyBtn) { - e.preventDefault(); - const topicId = parseInt(topicBuyBtn.getAttribute('data-aksrt-pay-topic') || '0', 10); - if (topicId) openCashier(topicId, topicBuyBtn, '', '', 'topic'); - return; - } - - const plateFollowBuyBtn = e.target.closest('[data-aksrt-pay-plate-follow]'); - if (plateFollowBuyBtn) { - e.preventDefault(); - const plateId = parseInt(plateFollowBuyBtn.getAttribute('data-aksrt-pay-plate-follow') || '0', 10); - if (plateId) openCashier(plateId, plateFollowBuyBtn, '', '', 'plate_follow'); - return; - } - const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]'); if (buyBtn) { e.preventDefault(); @@ -743,7 +706,7 @@ requestDownload(btn); }); - // Bridge for forum-governance plate money cashier. + // Public cashier bridge (content pay). window.aksrtOpenCashier = function (postId, returnFocusEl, priceType, couponCode, apiKind) { openCashier(postId, returnFocusEl || null, priceType || '', couponCode || '', apiKind || 'content'); }; diff --git a/assets/js/core/ajax-list.js b/assets/js/core/ajax-list.js index 5d9f9a1e..ec651911 100644 --- a/assets/js/core/ajax-list.js +++ b/assets/js/core/ajax-list.js @@ -248,7 +248,7 @@ closestPager(link); if (!container) { // Fall back: parent stream list. - container = link.closest(".aksrt-product-grid, .aksrt-forum-topics__list, [data-aksrt-ajax-list]"); + container = link.closest("[data-aksrt-ajax-list]"); } if (!container) return Promise.resolve(); diff --git a/assets/js/forum-edit.js b/assets/js/forum-edit.js deleted file mode 100644 index 2cf0043b..00000000 --- a/assets/js/forum-edit.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * AKSRT Forum Edit — 帖子编辑页交互 - * - * 处理论坛帖子编辑页的表单增强和标签输入。 - * - * @package AKSRT - */ - -(function () { - 'use strict'; - - var form = document.querySelector('.aksrt-forum-edit__form'); - if (!form) return; - - // 标签输入增强:逗号分隔转标签 chips - var tagInput = form.querySelector('#topic_tags'); - if (tagInput) { - tagInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - } - }); - } - - // 表单提交确认 - form.addEventListener('submit', function (e) { - var title = form.querySelector('#topic_title'); - var content = form.querySelector('#topic_content'); - - if (title && title.value.trim() === '') { - e.preventDefault(); - alert('标题不能为空'); - title.focus(); - return; - } - - // TinyMCE 检查 - if (typeof tinyMCE !== 'undefined' && tinyMCE.get('topic_content')) { - var editorContent = tinyMCE.get('topic_content').getContent().trim(); - if (editorContent === '') { - e.preventDefault(); - alert('内容不能为空'); - return; - } - } - }); -})(); diff --git a/assets/js/forum-governance.js b/assets/js/forum-governance.js deleted file mode 100644 index ea89be4f..00000000 --- a/assets/js/forum-governance.js +++ /dev/null @@ -1,1623 +0,0 @@ -(function () { - 'use strict'; - - var rootData = window.aksrtData || {}; - var restRoot = (function () { - var siteUrl = String(rootData.siteUrl || '/').replace(/\/+$/, ''); - return siteUrl + '/wp-json/aksrt/v1'; - })(); - - function request(path, options) { - var config = options || {}; - var headers = config.headers || {}; - - return fetch(restRoot + path, { - method: config.method || 'GET', - headers: Object.assign({ - 'Content-Type': 'application/json', - 'X-WP-Nonce': rootData.restNonce || '', - }, headers), - body: config.body || null, - credentials: 'same-origin', - }).then(function (response) { - return response.json().catch(function () { - return {}; - }).then(function (payload) { - if (!response.ok || payload.success === false) { - throw new Error(payload.message || ('HTTP ' + response.status)); - } - return payload; - }); - }); - } - - function setFeedback(node, text, state) { - if (!node) { - return; - } - - node.textContent = text || ''; - node.classList.remove('is-success', 'is-error', 'is-loading'); - if (state) { - node.classList.add(state); - } - } - - function bindComposeToolbar(form) { - // §7.4: toolbar replaced by wp_editor TinyMCE — no-op stub kept for boot() compat - } - - function insertTextAtCursor(textarea, text, after, start, end) { - var value = textarea.value; - textarea.value = value.substring(0, start) + text + (after || '') + value.substring(end); - textarea.focus(); - } - - function askUrlInline(anchor, placeholder, callback) { - // kept for potential quicktags fallback - var existing = document.querySelector('.aksrt-inline-url-input'); - if (existing) existing.remove(); - - var input = document.createElement('input'); - input.type = 'url'; - input.className = 'aksrt-inline-url-input'; - input.placeholder = placeholder || 'https://'; - input.value = 'https://'; - input.style.cssText = 'display:block;width:100%;max-width:320px;margin:4px 0;padding:6px 10px;border:1px solid #dce3ec;border-radius:8px;font-size:13px;'; - - var parent = anchor.parentElement; - parent.insertBefore(input, anchor.nextSibling); - input.focus(); - input.select(); - - function done() { - var val = input.value.trim(); - input.remove(); - callback(val); - } - - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { e.preventDefault(); done(); } - if (e.key === 'Escape') { input.remove(); callback(''); } - }); - input.addEventListener('blur', function () { done(); }); - } - - function bindComposeVoteToggle(form) { - var toggle = form.querySelector('#compose-vote-enabled'); - var fields = form.querySelector('.aksrt-forum-compose__vote-fields'); - if (!toggle || !fields) { - return; - } - - function sync() { - fields.hidden = !toggle.checked; - } - - toggle.addEventListener('change', sync); - sync(); - } - - function getEditorContent() { - if (typeof tinymce !== 'undefined') { - var editor = tinymce.get('compose-content'); - if (editor && !editor.isHidden()) { - return editor.getContent(); - } - } - var textarea = document.querySelector('#compose-content'); - return textarea ? textarea.value : ''; - } - - function bindComposeDraft(form) { - var draftButton = form.querySelector('#compose-draft'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - - if (!draftButton || !ajaxUrl || !nonce) { - return; - } - - draftButton.addEventListener('click', function () { - draftButton.disabled = true; - setFeedback(feedback, '正在保存草稿…', 'is-loading'); - - var data = new FormData(form); - data.set('content', getEditorContent()); - data.append('action', 'aksrt_forum_save_draft'); - data.append('_nonce', nonce); - - var draftId = form.getAttribute('data-draft-id') || '0'; - if (draftId && draftId !== '0') { - data.append('draft_id', draftId); - } - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '保存失败'); - } - if (payload.data && payload.data.post_id) { - form.setAttribute('data-draft-id', String(payload.data.post_id)); - } - setFeedback(feedback, payload.data.message || '草稿已保存', 'is-success'); - draftButton.disabled = false; - }).catch(function (error) { - setFeedback(feedback, error.message || '保存失败', 'is-error'); - draftButton.disabled = false; - }); - }); - } - - function bindComposeSubmit(form) { - var submitButton = form.querySelector('#compose-submit'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - - if (!submitButton || !ajaxUrl || !nonce) { - return; - } - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var board = form.querySelector('#compose-board') || form.querySelector('[name="board_id"]'); - var title = form.querySelector('#compose-title'); - var content = getEditorContent(); - - if (!board || !board.value || !title || !title.value.trim() || !content.trim()) { - setFeedback(feedback, '请填写版块、标题和主题内容。', 'is-error'); - return; - } - - submitButton.disabled = true; - setFeedback(feedback, '正在提交主题…', 'is-loading'); - - var data = new FormData(form); - data.set('content', content); - data.append('action', 'aksrt_forum_create_topic'); - data.append('_nonce', nonce); - - var draftId = form.getAttribute('data-draft-id') || '0'; - if (draftId && draftId !== '0') { - data.append('draft_id', draftId); - } - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (response) { - return response.json(); - }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '提交失败'); - } - - if (payload.data && payload.data.redirect) { - window.location.href = payload.data.redirect; - return; - } - - setFeedback(feedback, (payload.data && payload.data.message) || '主题已提交。', 'is-success'); - }).catch(function (error) { - setFeedback(feedback, error.message || '提交失败,请稍后重试。', 'is-error'); - submitButton.disabled = false; - }); - }); - } - - function bindModeratorApplyForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var boardId = parseInt(form.getAttribute('data-board-id') || '0', 10); - var reasonField = form.querySelector('[name="reason"]'); - var reason = reasonField ? reasonField.value.trim() : ''; - - if (!boardId || !reason) { - setFeedback(feedback, '请填写完整的申请理由。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - setFeedback(feedback, '正在提交申请…', 'is-loading'); - - request('/forum/moderator/apply', { - method: 'POST', - body: JSON.stringify({ - board_id: boardId, - reason: reason, - }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '申请已提交', 'is-success'); - if (submitButton) submitButton.disabled = true; - // Disable the form to prevent re-submission - var reasonField = form.querySelector('[name="reason"]'); - if (reasonField) reasonField.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '申请提交失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function bindVoteForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var topicId = parseInt(form.getAttribute('data-topic-id') || '0', 10); - var checked = Array.prototype.slice.call(form.querySelectorAll('input[name="option_ids[]"]:checked')).map(function (input) { - return input.value; - }); - - if (!topicId || checked.length === 0) { - setFeedback(feedback, '请选择至少一个投票选项。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - setFeedback(feedback, '正在提交投票…', 'is-loading'); - - request('/forum/topics/' + topicId + '/vote', { - method: 'POST', - body: JSON.stringify({ option_ids: checked }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '投票成功', 'is-success'); - // Disable all checkboxes and submit after vote - var checkboxes = form.querySelectorAll('input[type="checkbox"]'); - for (var i = 0; i < checkboxes.length; i++) checkboxes[i].disabled = true; - if (submitButton) submitButton.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '投票失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function buildModerationPayload(form, action) { - var noteField = form.querySelector('[name="note"]'); - var payload = { - action: action, - note: noteField ? noteField.value.trim() : '', - }; - - if (action === 'review') { - payload.review_status = (form.querySelector('[name="review_status"]') || {}).value || ''; - } else if (action === 'move_board') { - payload.target_board_id = parseInt((form.querySelector('[name="target_board_id"]') || {}).value || '0', 10); - } else if (action === 'visibility') { - payload.visibility = (form.querySelector('[name="visibility"]') || {}).value || ''; - if (payload.visibility === 'credits') { - payload.credits_cost = parseInt((form.querySelector('[name="credits_cost"]') || {}).value || '0', 10); - } else if (payload.visibility === 'pay') { - payload.pay_price = (form.querySelector('[name="pay_price"]') || {}).value || ''; - } else if (payload.visibility === 'role') { - payload.visibility_roles = Array.prototype.slice.call(form.querySelectorAll('[name="visibility_roles[]"] option:checked')).map(function (option) { - return option.value; - }); - } - } else if (action === 'sticky') { - // 置顶分级(§7.3):发送 0~3 整数,而非布尔值。 - payload.value = parseInt((form.querySelector('[name="sticky_value"]') || {}).value || '0', 10); - } else if (action === 'question_status') { - payload.value = parseInt((form.querySelector('[name="question_status_value"]') || {}).value || '0', 10); - } else if (action === 'delete' || action === 'undelete') { - payload.value = true; - } else { - var valueFieldName = action + '_value'; - payload.value = ((form.querySelector('[name="' + valueFieldName + '"]') || {}).value || '0') === '1'; - } - - return payload; - } - - function bindModerationFieldToggle(form) { - var actionField = form.querySelector('.js-aksrt-moderation-action'); - var groups = form.querySelectorAll('.js-aksrt-moderation-field'); - var visibilitySelect = form.querySelector('[name="visibility"]'); - var visibilityExtras = form.querySelectorAll('.js-aksrt-visibility-extra'); - if (!actionField || !groups.length) { - return; - } - - function syncVisibilityExtras() { - if (!visibilitySelect || !visibilityExtras.length) { - return; - } - var visibility = visibilitySelect.value; - visibilityExtras.forEach(function (group) { - group.hidden = actionField.value !== 'visibility' || group.getAttribute('data-visibility-field') !== visibility; - }); - } - - function sync() { - var action = actionField.value; - groups.forEach(function (group) { - var fieldName = group.getAttribute('data-field'); - if (group.classList.contains('js-aksrt-visibility-extra')) { - return; - } - group.hidden = fieldName !== action; - }); - syncVisibilityExtras(); - } - - actionField.addEventListener('change', sync); - if (visibilitySelect) { - visibilitySelect.addEventListener('change', syncVisibilityExtras); - } - sync(); - } - - function bindModerationForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - bindModerationFieldToggle(form); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var topicId = parseInt(form.getAttribute('data-topic-id') || '0', 10); - var actionField = form.querySelector('.js-aksrt-moderation-action'); - var action = actionField ? actionField.value : ''; - if (!topicId || !action) { - setFeedback(feedback, '请选择治理动作。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - if (action === 'delete' && !window.confirm('确认将该主题移入回收站吗?')) { - if (submitButton) submitButton.disabled = false; - return; - } - if (action === 'undelete' && !window.confirm('确认恢复该主题吗?')) { - if (submitButton) submitButton.disabled = false; - return; - } - setFeedback(feedback, '正在执行治理动作…', 'is-loading'); - - request('/forum/topics/' + topicId + '/moderate', { - method: 'POST', - body: JSON.stringify(buildModerationPayload(form, action)), - }).then(function (payload) { - setFeedback(feedback, payload.message || '治理动作已执行', 'is-success'); - if (submitButton) submitButton.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '治理动作执行失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function bindQuickTopicAction(button) { - button.addEventListener('click', function () { - var topicId = parseInt(button.getAttribute('data-topic-id') || '0', 10); - var action = button.getAttribute('data-action') || ''; - var rawValue = button.getAttribute('data-value') || '0'; - var valueType = button.getAttribute('data-value-type') || 'bool'; - // 置顶分级(§7.3):data-value-type="int" 时发送整数级别,否则发送布尔值。 - var value = valueType === 'int' ? parseInt(rawValue, 10) : rawValue === '1'; - - if (!topicId || !action) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/moderate', { - method: 'POST', - body: JSON.stringify({ - action: action, - value: value, - }), - }).then(function () { - // Toggle button visual state instead of reloading - var icon = button.querySelector('i'); - if (valueType === 'int' ? value > 0 : value) { - button.classList.add('is-active'); - if (icon) icon.className = icon.className.replace('far', 'fas'); - } else { - button.classList.remove('is-active'); - if (icon) icon.className = icon.className.replace('fas', 'far'); - } - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '治理动作执行失败。'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindFavoriteButton(button) { - button.addEventListener('click', function () { - var topicId = button.getAttribute('data-topic-id'); - if (!topicId || button.disabled) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/favorite', { - method: 'POST', - body: JSON.stringify({}), - }).then(function (payload) { - var isFav = !!(payload.favorited); - var icon = button.querySelector('i'); - var label = button.querySelector('.js-aksrt-favorite-label'); - button.setAttribute('data-favorited', isFav ? '1' : '0'); - if (icon) { - icon.className = isFav ? 'fas fa-bookmark' : 'far fa-bookmark'; - } - if (label) { - label.textContent = isFav ? '已收藏' : '收藏'; - } - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '操作失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindScoreButton(button) { - button.addEventListener('click', function () { - var container = button.closest('.aksrt-forum-topic__score-actions'); - if (!container || button.disabled) { - return; - } - - var topicId = container.getAttribute('data-topic-id'); - var amount = parseInt(button.getAttribute('data-amount') || '0', 10); - var feedback = container.querySelector('.js-aksrt-action-feedback'); - - if (!topicId || amount === 0) { - return; - } - - button.disabled = true; - setFeedback(feedback, '提交中…', 'is-loading'); - - request('/forum/topics/' + topicId + '/score', { - method: 'POST', - body: JSON.stringify({ amount: amount }), - }).then(function (payload) { - var scoreEl = container.querySelector('.js-aksrt-score-value'); - if (scoreEl && payload.score !== undefined) { - scoreEl.textContent = String(payload.score); - } - setFeedback(feedback, payload.message || '评分成功', 'is-success'); - // Disable both score buttons after success (one-time per user) - container.querySelectorAll('.js-aksrt-topic-score').forEach(function (btn) { - btn.disabled = true; - }); - }).catch(function (error) { - setFeedback(feedback, error.message || '评分失败', 'is-error'); - button.disabled = false; - }); - }); - } - - function bindAdoptButton(button) { - button.addEventListener('click', function () { - var topicId = button.getAttribute('data-topic-id'); - var commentId = button.getAttribute('data-comment-id'); - if (!topicId || !commentId || button.disabled) { - return; - } - - if (!window.confirm('确认采纳该回复为最佳答案?')) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/adopt', { - method: 'POST', - body: JSON.stringify({ comment_id: parseInt(commentId, 10) }), - }).then(function (payload) { - button.textContent = '✓ 已采纳'; - button.classList.add('is-adopted'); - // Disable all other adopt buttons - document.querySelectorAll('.js-aksrt-adopt-answer').forEach(function (btn) { - btn.disabled = true; - }); - }).catch(function (error) { - var msg = error.message || '采纳失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindReportModal(modal) { - var form = modal.querySelector('.js-aksrt-report-form'); - var feedback = modal.querySelector('.js-aksrt-action-feedback'); - var targetLabel = modal.querySelector('.js-aksrt-report-target'); - var targetTypeField = form ? form.querySelector('[name="target_type"]') : null; - var targetIdField = form ? form.querySelector('[name="target_id"]') : null; - var closeButtons = modal.querySelectorAll('.js-aksrt-report-close'); - - function openModal(trigger) { - if (!form) return; - var targetType = trigger.getAttribute('data-target-type') || 'post'; - var targetId = trigger.getAttribute('data-target-id') || ''; - var targetTitle = trigger.getAttribute('data-target-title') || ''; - if (targetTypeField) targetTypeField.value = targetType; - if (targetIdField) targetIdField.value = targetId; - if (targetLabel) { - targetLabel.textContent = '举报对象:' + (targetTitle || ('#' + targetId)); - } - modal.hidden = false; - document.body.classList.add('is-report-modal-open'); - } - - function closeModal() { - modal.hidden = true; - document.body.classList.remove('is-report-modal-open'); - if (form) form.reset(); - setFeedback(feedback, '', ''); - } - - document.querySelectorAll('.js-aksrt-report-trigger').forEach(function (trigger) { - trigger.addEventListener('click', function () { - openModal(trigger); - }); - }); - - closeButtons.forEach(function (btn) { - btn.addEventListener('click', closeModal); - }); - - if (!form) return; - - form.addEventListener('submit', function (event) { - event.preventDefault(); - var reason = (form.querySelector('[name="reason"]') || {}).value || ''; - if (!reason.trim()) { - setFeedback(feedback, '请填写举报说明。', 'is-error'); - return; - } - setFeedback(feedback, '正在提交举报…', 'is-loading'); - request('/report/submit', { - method: 'POST', - body: JSON.stringify({ - target_type: targetTypeField ? targetTypeField.value : 'post', - target_id: parseInt(targetIdField ? targetIdField.value : '0', 10), - category: (form.querySelector('[name="category"]') || {}).value || 'other', - reason: reason, - }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '举报已提交', 'is-success'); - setTimeout(closeModal, 900); - }).catch(function (error) { - setFeedback(feedback, error.message || '举报提交失败。', 'is-error'); - }); - }); - } - - - function insertForumEditorContent(html) { - if (window.tinymce) { - var ed = window.tinymce.get('compose-content') || window.tinymce.get('post_content'); - if (ed && !ed.isHidden()) { - ed.focus(); - ed.execCommand('mceInsertContent', false, html); - return; - } - } - var ta = document.getElementById('compose-content') || document.getElementById('post_content'); - if (!ta) return; - var start = ta.selectionStart || 0; - var end = ta.selectionEnd || 0; - var val = ta.value || ''; - ta.value = val.slice(0, start) + html + val.slice(end); - ta.focus(); - } - - function bindComposeShortcodeTools(root) { - root = root || document; - root.addEventListener('click', function (e) { - var btn = e.target.closest('[data-insert-shortcode]'); - if (!btn) return; - e.preventDefault(); - var type = btn.getAttribute('data-insert-shortcode'); - if (type === 'hide') { - insertForumEditorContent('[hidecontent type="login"]隐藏内容[/hidecontent]'); - } else if (type === 'reply') { - insertForumEditorContent('[hidecontent type="reply"]回复后可见[/hidecontent]'); - } else if (type === 'payshow') { - insertForumEditorContent('[hidecontent type="payshow"]付费可见内容[/hidecontent]'); - } else if (type === 'vip') { - insertForumEditorContent('[vip]VIP 可见内容[/vip]'); - } - }); - } - - function bindComposeVisibilityToggle(form) { - var visSelect = form.querySelector('#compose-visibility'); - var creditsField = form.querySelector('#compose-credits-cost-field'); - var payField = form.querySelector('#compose-pay-price-field'); - var roleField = form.querySelector('#compose-role-field'); - - if (!visSelect) return; - - function syncVisFields() { - var val = visSelect.value; - if (creditsField) creditsField.style.display = val === 'credits' ? '' : 'none'; - if (payField) payField.style.display = val === 'pay' ? '' : 'none'; - if (roleField) roleField.style.display = val === 'role' ? '' : 'none'; - } - - visSelect.addEventListener('change', syncVisFields); - syncVisFields(); - } - - function bindUnlockButton(button) { - button.addEventListener('click', function () { - var topicId = parseInt(button.getAttribute('data-topic-id') || '0', 10); - var cost = button.getAttribute('data-cost') || '0'; - var feedback = button.parentElement ? button.parentElement.querySelector('.js-aksrt-action-feedback') : null; - - if (!topicId || !window.confirm('确认使用 ' + cost + ' 积分解锁此主题?')) { - return; - } - - button.disabled = true; - setFeedback(feedback, '正在解锁…', 'is-loading'); - - var data = new FormData(); - data.append('action', 'aksrt_forum_unlock_topic'); - data.append('topic_id', topicId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '解锁失败'); - } - setFeedback(feedback, '解锁成功,正在刷新…', 'is-success'); - setTimeout(function () { window.location.reload(); }, 800); - }).catch(function (error) { - setFeedback(feedback, error.message || '解锁失败', 'is-error'); - button.disabled = false; - }); - }); - } - - function formatFollowPayLabel(pay) { - pay = pay || {}; - if (pay.is_free) { - return 'VIP 免费关注'; - } - if (pay.label) { - return String(pay.label); - } - if (pay.modo === 'points') { - return String(pay.amount || 0) + ' 积分'; - } - return '¥' + String(pay.amount || '0'); - } - - function purchasePlateFollow(plateId, button, payHint) { - payHint = payHint || {}; - var modo = String(payHint.modo || 'money'); - - // Cash (paid or VIP free): multi-gateway cashier — free VIP still creates 0-order for ledger. - // No Credits×100 bypass. - if (modo === 'money') { - button.disabled = false; - if (typeof window.aksrtOpenCashier === 'function') { - window.aksrtOpenCashier(plateId, button, '', '', 'plate_follow'); - return; - } - var msgCashier = '收银台未加载,请刷新页面后重试'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msgCashier, 'error'); } - else { window.alert(msgCashier); } - return; - } - - // Points / points VIP free: keep AJAX purchase path. - var confirmMsg = '确认付费关注该版块?'; - if (payHint.is_free) { - confirmMsg = '确认使用 VIP 免费关注该版块?'; - } else { - confirmMsg = '确认支付 ' + formatFollowPayLabel(payHint) + ' 关注该版块?'; - } - if (!window.confirm(confirmMsg)) { - button.disabled = false; - return; - } - - var data = new FormData(); - data.append('action', 'aksrt_forum_purchase_plate_follow'); - data.append('plate_id', plateId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - var errData = payload.data || {}; - // Server may redirect money path to cashier. - if (errData.require_cashier && typeof window.aksrtOpenCashier === 'function') { - button.disabled = false; - window.aksrtOpenCashier(plateId, button, '', '', 'plate_follow'); - return; - } - throw new Error(errData.message || '支付失败'); - } - var res = payload.data || {}; - button.setAttribute('data-followed', '1'); - button.classList.add('is-followed'); - if (button.querySelector('i') && button.childNodes.length) { - button.innerHTML = ' 已关注'; - } else { - button.textContent = '已关注'; - } - if (typeof window.aksrtToast === 'function') { - window.aksrtToast(res.message || '关注成功', 'success'); - } - setTimeout(function () { window.location.reload(); }, 600); - }).catch(function (error) { - var msg = error.message || '支付失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - } - - function bindFollowPlateButton(button) { - button.addEventListener('click', function () { - var plateId = parseInt(button.getAttribute('data-plate-id') || '0', 10); - if (!plateId || button.disabled) { - return; - } - button.disabled = true; - - var data = new FormData(); - data.append('action', 'aksrt_forum_follow_plate'); - data.append('plate_id', plateId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - // Paid follow required - if (!payload.success && payload.data && payload.data.require_pay) { - purchasePlateFollow(plateId, button, payload.data.pay || {}); - return; - } - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '操作失败'); - } - var res = payload.data || {}; - var followed = !!res.followed; - button.setAttribute('data-followed', followed ? '1' : '0'); - button.classList.toggle('is-followed', followed); - button.innerHTML = followed - ? ' 已关注' - : ' 关注'; - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '操作失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindComposeCoverToggle(form) { - var coverTypeSelect = form.querySelector('#compose-cover-type'); - var imageField = form.querySelector('#compose-cover-image-field'); - var slideField = form.querySelector('#compose-cover-slide-field'); - var videoField = form.querySelector('#compose-cover-video-field'); - - if (!coverTypeSelect) return; - - function syncCoverFields() { - var val = coverTypeSelect.value; - if (imageField) imageField.style.display = val === 'image' ? '' : 'none'; - if (slideField) slideField.style.display = val === 'slide' ? '' : 'none'; - if (videoField) videoField.style.display = val === 'video' ? '' : 'none'; - } - - coverTypeSelect.addEventListener('change', syncCoverFields); - syncCoverFields(); - - var imageInput = form.querySelector('#compose-cover-image'); - var imagePreview = form.querySelector('#compose-cover-preview'); - var imageBtn = form.querySelector('#compose-cover-upload-btn'); - - if (imageBtn && typeof wp !== 'undefined' && wp.media) { - imageBtn.addEventListener('click', function () { - var frame = wp.media({ title: '选择封面图片', multiple: false, library: { type: 'image' } }); - frame.on('select', function () { - var attachment = frame.state().get('selection').first().toJSON(); - if (imageInput) imageInput.value = attachment.url; - if (imagePreview) imagePreview.innerHTML = ''; - }); - frame.open(); - }); - } - - var slideInput = form.querySelector('#compose-featured-slide'); - var slidePreview = form.querySelector('#compose-slide-preview'); - var slideBtn = form.querySelector('#compose-slide-upload-btn'); - - if (slideBtn && typeof wp !== 'undefined' && wp.media) { - slideBtn.addEventListener('click', function () { - var frame = wp.media({ title: '选择幻灯片图片', multiple: true, library: { type: 'image' } }); - frame.on('select', function () { - var urls = frame.state().get('selection').map(function (a) { return a.toJSON().url; }); - urls = urls.slice(0, 9); - if (slideInput) slideInput.value = JSON.stringify(urls); - if (slidePreview) { - slidePreview.innerHTML = urls.map(function (u) { return ''; }).join(''); - } - }); - frame.open(); - }); - } - } - - function bindEditPlateButton(button) { - button.addEventListener('click', function () { - var plateId = button.getAttribute('data-plate-id'); - var name = window.prompt('版块名称:', button.getAttribute('data-name') || ''); - if (name === null) return; - var description = window.prompt('版块描述:', button.getAttribute('data-description') || ''); - if (description === null) description = ''; - - request('/forum/front/plates/' + plateId, { - method: 'POST', - body: JSON.stringify({ name: name, description: description }), - }).then(function () { - window.location.reload(); - }).catch(function (error) { - window.alert(error.message || '编辑失败'); - }); - }); - } - - function bindDeletePlateButton(button) { - button.addEventListener('click', function () { - var plateId = button.getAttribute('data-plate-id'); - var topicCount = parseInt(button.getAttribute('data-topic-count') || '0', 10); - var msg = '确认删除此版块?'; - if (topicCount > 0) { - msg += '\n该版块下有 ' + topicCount + ' 个主题,删除后将移入回收站。'; - } - if (!window.confirm(msg)) return; - - request('/forum/front/plates/' + plateId, { - method: 'DELETE', - body: JSON.stringify({ cascade: 'trash' }), - }).then(function () { - window.location.href = (rootData.siteUrl || '') + '/forums/'; - }).catch(function (error) { - window.alert(error.message || '删除失败'); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Remote search selector - // ────────────────────────────────────────────────────────── - - function bindRemoteSelects(container) { - var selects = container.querySelectorAll('.aksrt-remote-select'); - selects.forEach(function (wrapper) { - var searchAction = wrapper.getAttribute('data-search-action'); - var hiddenInput = wrapper.querySelector('input[type="hidden"]'); - var textInput = wrapper.querySelector('.js-aksrt-remote-input'); - var dropdown = wrapper.querySelector('.aksrt-remote-select__dropdown'); - if (!searchAction || !hiddenInput || !textInput || !dropdown) return; - - var debounceTimer = null; - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - - textInput.addEventListener('input', function () { - clearTimeout(debounceTimer); - var q = textInput.value.trim(); - debounceTimer = setTimeout(function () { - fetch(ajaxUrl + '?action=' + encodeURIComponent(searchAction) + '&q=' + encodeURIComponent(q), { - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success || !payload.data) return; - dropdown.innerHTML = ''; - payload.data.forEach(function (item) { - var div = document.createElement('div'); - div.className = 'aksrt-remote-select__option'; - div.textContent = item.name; - div.setAttribute('data-id', String(item.id)); - div.addEventListener('mousedown', function (e) { - e.preventDefault(); - hiddenInput.value = item.id; - textInput.value = item.name; - dropdown.hidden = true; - }); - dropdown.appendChild(div); - }); - dropdown.hidden = payload.data.length === 0; - }); - }, 250); - }); - - textInput.addEventListener('focus', function () { - if (dropdown.children.length > 0) dropdown.hidden = false; - textInput.dispatchEvent(new Event('input')); - }); - textInput.addEventListener('blur', function () { - setTimeout(function () { dropdown.hidden = true; }, 200); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Tag tokenizer - // ────────────────────────────────────────────────────────── - - function bindTagInputs(container) { - var wrappers = container.querySelectorAll('.aksrt-tag-input'); - wrappers.forEach(function (wrapper) { - var searchAction = wrapper.getAttribute('data-search-action'); - var maxTags = parseInt(wrapper.getAttribute('data-max') || '5', 10); - var hiddenInput = wrapper.querySelector('input[type="hidden"]'); - var tokenContainer = wrapper.querySelector('.aksrt-tag-input__tokens'); - var textInput = wrapper.querySelector('.js-aksrt-tag-search'); - var dropdown = wrapper.querySelector('.aksrt-tag-input__dropdown'); - if (!hiddenInput || !tokenContainer || !textInput) return; - - var tags = []; - var initialVal = (hiddenInput.value || '').trim(); - if (initialVal) { - initialVal.split(/[,,]/).forEach(function (t) { - t = t.trim(); - if (t && tags.indexOf(t) === -1) tags.push(t); - }); - } - - function syncHidden() { - hiddenInput.value = tags.join(', '); - } - - function renderTokens() { - tokenContainer.innerHTML = ''; - tags.forEach(function (tag, idx) { - var span = document.createElement('span'); - span.className = 'aksrt-tag-input__token'; - span.textContent = tag; - var close = document.createElement('button'); - close.type = 'button'; - close.className = 'aksrt-tag-input__token-remove'; - close.innerHTML = '×'; - close.addEventListener('click', function () { - tags.splice(idx, 1); - syncHidden(); - renderTokens(); - }); - span.appendChild(close); - tokenContainer.appendChild(span); - }); - } - - function addTag(name) { - name = name.trim(); - if (!name || tags.indexOf(name) !== -1 || tags.length >= maxTags) return; - tags.push(name); - syncHidden(); - renderTokens(); - textInput.value = ''; - } - - renderTokens(); - - textInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - addTag(textInput.value); - if (dropdown) dropdown.hidden = true; - } - if (e.key === 'Backspace' && textInput.value === '' && tags.length > 0) { - tags.pop(); - syncHidden(); - renderTokens(); - } - }); - - if (searchAction && dropdown) { - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - var debounce = null; - - textInput.addEventListener('input', function () { - clearTimeout(debounce); - var q = textInput.value.trim(); - if (!q) { dropdown.hidden = true; return; } - debounce = setTimeout(function () { - fetch(ajaxUrl + '?action=' + encodeURIComponent(searchAction) + '&q=' + encodeURIComponent(q), { - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success || !payload.data) return; - dropdown.innerHTML = ''; - payload.data.forEach(function (item) { - var div = document.createElement('div'); - div.className = 'aksrt-tag-input__suggestion'; - div.textContent = item.name + (item.count !== undefined ? ' (' + item.count + ')' : ''); - div.addEventListener('mousedown', function (e) { - e.preventDefault(); - addTag(item.name); - dropdown.hidden = true; - }); - dropdown.appendChild(div); - }); - dropdown.hidden = payload.data.length === 0; - }); - }, 250); - }); - - textInput.addEventListener('blur', function () { - setTimeout(function () { dropdown.hidden = true; }, 200); - }); - } - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Title character counter - // ────────────────────────────────────────────────────────── - - function bindTitleCounter(form) { - var titleInput = form.querySelector('#compose-title'); - var counterCurrent = form.querySelector('.js-aksrt-counter-current'); - if (!titleInput || !counterCurrent) return; - - function update() { - counterCurrent.textContent = String(titleInput.value.length); - var max = parseInt(titleInput.getAttribute('data-aksrt-maxlength') || '30', 10); - var parent = counterCurrent.closest('.aksrt-forum-compose__char-counter'); - if (parent) { - parent.classList.toggle('is-over', titleInput.value.length > max); - parent.classList.toggle('is-near', titleInput.value.length > max * 0.8 && titleInput.value.length <= max); - } - } - - titleInput.addEventListener('input', update); - update(); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Draft list toggle & delete - // ────────────────────────────────────────────────────────── - - function bindDraftUI() { - var toggle = document.querySelector('.js-aksrt-draft-toggle'); - var list = document.querySelector('#compose-draft-list'); - if (toggle && list) { - toggle.addEventListener('click', function () { - list.hidden = !list.hidden; - var icon = toggle.querySelector('i.fa-chevron-down, i.fa-chevron-up'); - if (icon) { - icon.classList.toggle('fa-chevron-down'); - icon.classList.toggle('fa-chevron-up'); - } - }); - } - - document.querySelectorAll('.js-aksrt-delete-draft').forEach(function (btn) { - btn.addEventListener('click', function () { - var draftId = btn.getAttribute('data-draft-id'); - if (!draftId || !window.confirm('确认删除此草稿?')) return; - - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - var nonce = ''; - var form = document.querySelector('.js-aksrt-forum-compose-form'); - if (form) nonce = form.getAttribute('data-nonce') || ''; - - var data = new FormData(); - data.append('action', 'aksrt_forum_delete_draft'); - data.append('_nonce', nonce); - data.append('draft_id', draftId); - - fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }) - .then(function (r) { - return r.text().then(function (text) { - var payload = null; - try { - payload = text ? JSON.parse(text) : null; - } catch (e) { - payload = null; - } - return { ok: r.ok, status: r.status, payload: payload, raw: text }; - }); - }) - .then(function (res) { - if (res.payload && res.payload.success) { - var item = btn.closest('.aksrt-forum-compose__draft-item'); - if (item) item.remove(); - return; - } - var msg = (res.payload && res.payload.data && res.payload.data.message) - || (res.payload && res.payload.message) - || (res.status === 500 - ? '服务器错误:请确认主题已更新到最新 main(Forum.php 草稿 autoload 修复)' - : '删除失败'); - if (window.console && console.warn) { - console.warn('[aksrt] delete draft failed', res.status, msg); - } - window.alert(msg); - }) - .catch(function (err) { - if (window.console && console.warn) { - console.warn('[aksrt] delete draft network error', err); - } - window.alert('删除失败,请稍后重试'); - }); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Edit form AJAX submit - // ────────────────────────────────────────────────────────── - - function bindEditForm(form) { - var submitButton = form.querySelector('#compose-submit'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - var topicId = form.getAttribute('data-topic-id'); - - if (!submitButton || !ajaxUrl || !nonce || !topicId) return; - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var title = form.querySelector('#compose-title'); - var content = getEditorContent(); - - if (!title || !title.value.trim() || !content.trim()) { - setFeedback(feedback, '标题和内容不能为空。', 'is-error'); - return; - } - - submitButton.disabled = true; - setFeedback(feedback, '正在保存…', 'is-loading'); - - var data = new FormData(form); - data.set('content', content); - data.append('action', 'aksrt_forum_edit_topic'); - data.append('_nonce', nonce); - data.append('topic_id', topicId); - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '保存失败'); - } - if (payload.data && payload.data.redirect) { - window.location.href = payload.data.redirect; - return; - } - setFeedback(feedback, payload.data.message || '已保存', 'is-success'); - submitButton.disabled = false; - }).catch(function (error) { - setFeedback(feedback, error.message || '保存失败', 'is-error'); - submitButton.disabled = false; - }); - }); - } - - // ── §7.6 Forum comment engine ───────────────────────────────────────── - - function bindForumCommentEngine() { - var section = document.getElementById('aksrt-forum-replies'); - if (!section) return; - - var topicId = section.dataset.topicId; - var nonce = section.dataset.nonce; - var list = section.querySelector('.js-aksrt-forum-replies-list'); - var loadMore = section.querySelector('.js-aksrt-forum-load-more'); - var currentSort = 'newest'; - var currentAuthorOnly = false; - - // Sort tabs - section.querySelectorAll('.aksrt-forum-replies__tab').forEach(function (tab) { - tab.addEventListener('click', function () { - section.querySelectorAll('.aksrt-forum-replies__tab').forEach(function (t) { t.classList.remove('is-active'); }); - tab.classList.add('is-active'); - currentSort = tab.dataset.sort || 'newest'; - loadComments(1); - }); - }); - - // Author-only toggle - var authorToggle = section.querySelector('.aksrt-forum-replies__author-toggle'); - if (authorToggle) { - authorToggle.addEventListener('change', function () { - currentAuthorOnly = authorToggle.checked; - loadComments(1); - }); - } - - // Load more button - if (loadMore) { - loadMore.addEventListener('click', function () { - var page = parseInt(loadMore.dataset.page, 10) || 1; - loadComments(page + 1, true); - }); - } - - function loadComments(page, append) { - var fd = new FormData(); - fd.append('action', 'aksrt_forum_comments'); - fd.append('nonce', nonce); - fd.append('topic_id', topicId); - fd.append('sort', currentSort); - fd.append('author_only', currentAuthorOnly ? '1' : ''); - fd.append('page', page); - - list.style.opacity = append ? '1' : '0.5'; - - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : (window.ajaxurl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: fd, - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - list.style.opacity = '1'; - if (!res.success) return; - if (append) { - list.insertAdjacentHTML('beforeend', res.data.html); - } else { - list.innerHTML = res.data.html; - } - if (loadMore) { - loadMore.dataset.page = String(page); - loadMore.dataset.max = String(res.data.max_pages); - loadMore.closest('.aksrt-forum-replies__load-more').style.display = - res.data.has_more ? '' : 'none'; - } - bindDynamicCommentActions(list); - }).catch(function () { - list.style.opacity = '1'; - }); - } - - bindDynamicCommentActions(list); - } - - function bindDynamicCommentActions(container) { - if (!container) return; - container.querySelectorAll('.js-aksrt-comment-like').forEach(bindCommentLike); - container.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton); - container.querySelectorAll('.js-aksrt-comment-reply').forEach(bindCommentReply); - container.querySelectorAll('.js-aksrt-comment-top').forEach(bindCommentTop); - } - - function bindCommentLike(btn) { - if (btn._likebound) return; - btn._likebound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var active = btn.dataset.active === '1'; - var newActive = !active; - - btn.disabled = true; - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.restUrl + 'aksrt/v1/comment/like' : '/wp-json/aksrt/v1/comment/like', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-WP-Nonce': window.aksrtForumGovernance ? window.aksrtForumGovernance.restNonce : '' - }, - body: JSON.stringify({ comment_id: parseInt(commentId, 10), active: newActive }), - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - btn.disabled = false; - if (!res.success && !res.data) return; - var data = res.data || res; - btn.dataset.active = data.active ? '1' : '0'; - btn.classList.toggle('is-active', !!data.active); - var icon = btn.querySelector('i'); - if (icon) { - icon.className = data.active ? 'fas fa-thumbs-up' : 'far fa-thumbs-up'; - } - var count = btn.querySelector('.aksrt-forum-reply__like-count'); - if (count) { - count.textContent = data.count > 0 ? String(data.count) : ''; - } - }).catch(function () { btn.disabled = false; }); - }); - } - - function bindCommentReply(btn) { - if (btn._replybound) return; - btn._replybound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var author = btn.dataset.author || ''; - var form = document.querySelector('.aksrt-forum-topic__reply-form'); - if (!form) return; - var textarea = form.querySelector('textarea[name="comment"]'); - if (!textarea) return; - // Set reply parent - var parentInput = form.querySelector('input[name="comment_parent"]'); - if (!parentInput) { - parentInput = document.createElement('input'); - parentInput.type = 'hidden'; - parentInput.name = 'comment_parent'; - form.querySelector('form').appendChild(parentInput); - } - parentInput.value = commentId; - textarea.placeholder = '@' + author + ' '; - textarea.focus(); - form.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); - } - - function bindCommentTop(btn) { - if (btn._topbound) return; - btn._topbound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var currentLevel = parseInt(btn.dataset.level, 10) || 0; - var newLevel = currentLevel > 0 ? 0 : 1; - - btn.disabled = true; - var section = document.getElementById('aksrt-forum-replies'); - var nonce = section ? section.dataset.nonce : ''; - - var fd = new FormData(); - fd.append('action', 'aksrt_forum_comment_top'); - fd.append('nonce', nonce); - fd.append('comment_id', commentId); - fd.append('level', newLevel); - - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : (window.ajaxurl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: fd, - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - btn.disabled = false; - if (res.success) { - btn.dataset.level = String(newLevel); - var icon = btn.querySelector('i'); - btn.childNodes.forEach(function (n) { - if (n.nodeType === 3) n.textContent = newLevel > 0 ? ' 取消置顶' : ' 置顶'; - }); - // Reload comments to reflect pin ordering - var activeTab = section && section.querySelector('.aksrt-forum-replies__tab.is-active'); - if (activeTab) activeTab.click(); - } - }).catch(function () { btn.disabled = false; }); - }); - } - - // ── §7.10 Forum home tab engine ────────────────────────────────────── - - function bindForumHomeTabs() { - var nav = document.getElementById('aksrt-forum-home-tabs'); - if (!nav) return; - - var content = document.getElementById('aksrt-forum-home-content'); - if (!content) return; - - var nonce = nav.dataset.nonce || ''; - var ajaxUrl = nav.dataset.ajaxUrl || (window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : '/wp-admin/admin-ajax.php'); - - nav.querySelectorAll('.aksrt-forum-home-tabs__btn').forEach(function (btn) { - btn.addEventListener('click', function () { - var tabType = btn.dataset.tabType; - nav.querySelectorAll('.aksrt-forum-home-tabs__btn').forEach(function (b) { b.classList.remove('is-active'); }); - btn.classList.add('is-active'); - loadHomeTab(tabType, 1); - }); - }); - - // Load-more on home content - content.addEventListener('click', function (e) { - var moreBtn = e.target.closest('.js-aksrt-home-load-more'); - if (!moreBtn) return; - var page = parseInt(moreBtn.dataset.page, 10) || 1; - var tabType = moreBtn.dataset.tab || 'synthesis'; - loadHomeTab(tabType, page + 1, true, moreBtn); - }); - - function loadHomeTab(tabType, page, append, moreBtn) { - var fd = new FormData(); - fd.append('action', 'aksrt_forum_home_tab'); - fd.append('nonce', nonce); - fd.append('tab_type', tabType); - fd.append('page', page); - - if (!append) { - content.style.opacity = '0.5'; - } - if (moreBtn) moreBtn.disabled = true; - - fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - content.style.opacity = '1'; - if (!res.success) return; - - if (res.data.type === 'plate') { - // Plate tab — reload the whole page to get server-rendered board grid - window.location.href = window.location.pathname + '?forum_tab=plate'; - return; - } - - if (append) { - var list = content.querySelector('.js-aksrt-home-topic-list'); - if (list) { - list.insertAdjacentHTML('beforeend', res.data.html); - } - if (moreBtn) { - moreBtn.disabled = false; - moreBtn.dataset.page = String(page); - if (!res.data.has_more) { - moreBtn.closest('.aksrt-forum-home__load-more').style.display = 'none'; - } - } - } else { - var html = '
' + res.data.html + '
'; - if (res.data.has_more) { - html += '
' - + '
'; - } - content.innerHTML = html; - } - }).catch(function () { - content.style.opacity = '1'; - if (moreBtn) moreBtn.disabled = false; - }); - } - } - - - function bindBoardSortTabs() { - var root = document.querySelector('.aksrt-forum-board[data-plate-id], .aksrt-forum-board[data-board-id]'); - if (!root) return; - var list = root.querySelector('.js-aksrt-board-topic-list'); - var tabs = root.querySelectorAll('.js-aksrt-board-sort-tab'); - if (!list || !tabs.length) return; - - var plateId = root.getAttribute('data-plate-id') || ''; - var boardId = root.getAttribute('data-board-id') || ''; - var nonce = root.getAttribute('data-nonce') || (window.aksrtForumGovernance && aksrtForumGovernance.publicNonce) || (window.aksrtData && aksrtData.nonce) || ''; - var ajaxUrl = (window.aksrtForumGovernance && aksrtForumGovernance.ajaxUrl) || (window.aksrtData && aksrtData.ajaxUrl) || ''; - - function loadSort(sort, pushUrl) { - if (!ajaxUrl) return; - list.style.opacity = '0.55'; - var fd = new FormData(); - fd.append('action', 'aksrt_forum_board_topics'); - fd.append('nonce', nonce); - fd.append('page', '1'); - fd.append('orderby', sort); - if (plateId) fd.append('plate_id', plateId); - if (boardId) fd.append('board_id', boardId); - - fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - list.style.opacity = '1'; - if (!res || !res.success) return; - list.innerHTML = res.data.html || ''; - root.setAttribute('data-active-sort', sort); - tabs.forEach(function (t) { - t.classList.toggle('is-active', (t.getAttribute('data-sort') || '') === sort); - }); - if (pushUrl && window.history && history.replaceState) { - try { - var u = new URL(window.location.href); - u.searchParams.set('forum_sort', sort); - history.replaceState({}, '', u.toString()); - } catch (e) {} - } - }) - .catch(function () { list.style.opacity = '1'; }); - } - - tabs.forEach(function (tab) { - tab.addEventListener('click', function (e) { - e.preventDefault(); - var sort = tab.getAttribute('data-sort') || 'date'; - if (sort === (root.getAttribute('data-active-sort') || '')) return; - loadSort(sort, true); - }); - }); - } - - function boot() { - var composeForm = document.querySelector('.js-aksrt-forum-compose-form'); - if (composeForm) { - bindComposeToolbar(composeForm); - bindComposeVoteToggle(composeForm); - bindComposeCoverToggle(composeForm); - bindComposeVisibilityToggle(composeForm); - bindComposeDraft(composeForm); - bindComposeSubmit(composeForm); - bindRemoteSelects(composeForm); - bindTagInputs(composeForm); - bindTitleCounter(composeForm); - bindDraftUI(); - } - - var editForm = document.querySelector('.js-aksrt-forum-edit-form'); - if (editForm) { - bindComposeVoteToggle(editForm); - bindComposeCoverToggle(editForm); - bindComposeVisibilityToggle(editForm); - bindRemoteSelects(editForm); - bindTagInputs(editForm); - bindTitleCounter(editForm); - bindEditForm(editForm); - } - - document.querySelectorAll('.js-aksrt-moderator-apply-form').forEach(bindModeratorApplyForm); - document.querySelectorAll('.js-aksrt-topic-vote-form').forEach(bindVoteForm); - document.querySelectorAll('.js-aksrt-topic-moderation-form').forEach(bindModerationForm); - document.querySelectorAll('.js-aksrt-topic-action').forEach(bindQuickTopicAction); - document.querySelectorAll('.js-aksrt-topic-favorite').forEach(bindFavoriteButton); - document.querySelectorAll('.js-aksrt-topic-score').forEach(bindScoreButton); - document.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton); - document.querySelectorAll('.js-aksrt-unlock-topic').forEach(bindUnlockButton); - document.querySelectorAll('.js-aksrt-follow-plate').forEach(bindFollowPlateButton); - bindForumCommentEngine(); - bindForumHomeTabs(); - bindBoardSortTabs(); - document.querySelectorAll('.js-aksrt-edit-plate').forEach(bindEditPlateButton); - document.querySelectorAll('.js-aksrt-delete-plate').forEach(bindDeletePlateButton); - var reportModal = document.querySelector('#aksrt-report-modal'); - if (reportModal) { - bindReportModal(reportModal); - } - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', boot); - } else { - boot(); - } -})(); - diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 09f80630..d6e09ed3 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -89,10 +89,6 @@ return suffix ? base + suffix : base; } - function cartUrl() { - return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/"; - } - /* ── Order review modal (zibll 1:1) ── */ var STAR_SVG = ''; @@ -434,7 +430,7 @@ badge.textContent = statusLabel || newStatus; } // Hide action buttons that are no longer relevant - var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel], [data-aksrt-order-after-sale], [data-aksrt-order-receipt], [data-aksrt-order-rebuy]"); + var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel]"); for (var i = 0; i < actions.length; i++) { actions[i].style.display = "none"; } @@ -640,21 +636,9 @@ // Two-step cancel inside the modal — no window.confirm. footer += ''; } - if (actions.can_receipt) { - footer += ''; - } - if (actions.can_review) { - footer += ''; - } if (actions.can_pay) { footer += ''; } - if (actions.can_after_sale) { - footer += ''; - } - if (actions.can_rebuy) { - footer += ''; - } if (actions.can_view_product && order.permalink) { footer += '查看商品'; } @@ -695,70 +679,6 @@ } } - /* ── After-Sale Modal ── */ - var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]"); - var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]"); - var afterSaleOrderInput = afterSaleForm ? afterSaleForm.querySelector('[name="order_number"]') : null; - - function openAfterSaleModal(orderNumber) { - if (!afterSaleModal || !afterSaleForm) { - showToast("售后表单加载失败,请刷新页面后重试。"); - return; - } - afterSaleForm.reset(); - if (afterSaleOrderInput) afterSaleOrderInput.value = orderNumber; - afterSaleModal.hidden = false; - } - - function closeAfterSaleModal() { - if (afterSaleModal) afterSaleModal.hidden = true; - } - - if (afterSaleForm) { - afterSaleForm.addEventListener("submit", function (event) { - event.preventDefault(); - var formData = new FormData(afterSaleForm); - var orderNumber = formData.get("order_number"); - var reason = (formData.get("reason") || "").trim(); - if (!reason) { - showToast("请填写售后原因。"); - return; - } - - var submitBtn = afterSaleForm.querySelector('button[type="submit"]'); - if (submitBtn) { - submitBtn.disabled = true; - submitBtn.textContent = "提交中..."; - } - - request(orderPath(orderNumber, "/after-sale"), { - type: formData.get("type") || "refund", - reason: reason, - note: (formData.get("note") || "").trim() - }).then(function (result) { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.textContent = "提交售后"; - } - if (!result.success) { - showToast(result.message || "提交售后失败。"); - return; - } - closeAfterSaleModal(); - showToast("售后申请已提交。"); - // Local refresh: update card status - var card = findOrderCard(orderNumber); - updateCardStatus(card, "refunded", "已退款"); - }).catch(function () { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.textContent = "提交售后"; - } - showToast("网络错误,请稍后重试。"); - }); - }); - } - /* ── Countdown timer ── */ function updateCountdowns() { var nodes = page.querySelectorAll("[data-aksrt-order-countdown]"); @@ -808,56 +728,6 @@ return; } - // Confirm receipt - var receiptBtn = event.target.closest("[data-aksrt-order-receipt]"); - if (receiptBtn) { - var receiptOrder = receiptBtn.getAttribute("data-aksrt-order-receipt"); - if (!receiptOrder) return; - if (!window.confirm("确认已收到货物?")) return; - receiptBtn.disabled = true; - request(orderPath(receiptOrder, "/confirm-receipt"), {}) - .then(function (result) { - receiptBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "确认收货失败"); - return; - } - showToast("已确认收货"); - updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成"); - if (detailOrderNumber === receiptOrder) { - closeOrderDetailModal(); - } - }) - .catch(function () { - receiptBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - - // Rebuy - var rebuyBtn = event.target.closest("[data-aksrt-order-rebuy]"); - if (rebuyBtn) { - var rebuyOrder = rebuyBtn.getAttribute("data-aksrt-order-rebuy"); - if (!rebuyOrder) return; - rebuyBtn.disabled = true; - request(orderPath(rebuyOrder, "/rebuy"), {}) - .then(function (result) { - rebuyBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "再次购买失败"); - return; - } - showToast("已加入购物车"); - window.location.href = cartUrl(); - }) - .catch(function () { - rebuyBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - // Review modal (zibll order_comment_modal 1:1 UX) var reviewBtn = event.target.closest("[data-aksrt-order-review]"); if (reviewBtn) { @@ -992,103 +862,8 @@ }); return; } - - // After-Sale - var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]"); - if (afterSaleButton) { - var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale"); - closeOrderDetailModal(); - openAfterSaleModal(afterSaleOrderNumber); - return; - } - - // Return shipping - var returnShipBtn = event.target.closest("[data-aksrt-return-ship]"); - if (returnShipBtn) { - openReturnShipModal(returnShipBtn.getAttribute("data-aksrt-return-ship")); - return; - } - - // Cancel after-sale ticket - var asCancelBtn = event.target.closest("[data-aksrt-as-cancel]"); - if (asCancelBtn) { - var tid = asCancelBtn.getAttribute("data-aksrt-as-cancel"); - if (!tid || !window.confirm("确认取消该售后申请?")) return; - asCancelBtn.disabled = true; - request("/shop/frontend/after-sale/" + tid + "/cancel", { reason: "" }) - .then(function (result) { - asCancelBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "取消失败"); - return; - } - showToast(result.message || "已取消售后"); - window.location.reload(); - }) - .catch(function () { - asCancelBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - - // Close after-sale modal - if (event.target.closest("[data-aksrt-aftersale-modal-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-aftersale-modal]"))) { - closeAfterSaleModal(); - return; - } - - // Close return-ship modal - if (event.target.closest("[data-aksrt-return-ship-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-return-ship-modal]"))) { - closeReturnShipModal(); - } }); - /* ── Return shipping modal ── */ - var returnShipModal = page.querySelector("[data-aksrt-return-ship-modal]"); - var returnShipForm = page.querySelector("[data-aksrt-return-ship-form]"); - function openReturnShipModal(ticketId) { - if (!returnShipModal || !returnShipForm) return; - returnShipForm.reset(); - var idInput = returnShipForm.querySelector('[name="ticket_id"]'); - if (idInput) idInput.value = ticketId || ""; - returnShipModal.hidden = false; - } - function closeReturnShipModal() { - if (returnShipModal) returnShipModal.hidden = true; - } - if (returnShipForm) { - returnShipForm.addEventListener("submit", function (event) { - event.preventDefault(); - var fd = new FormData(returnShipForm); - var tid = fd.get("ticket_id"); - var carrier = (fd.get("carrier") || "").trim(); - var tracking = (fd.get("tracking_no") || "").trim(); - if (!carrier || !tracking) { - showToast("请填写快递公司与运单号"); - return; - } - var btn = returnShipForm.querySelector('button[type="submit"]'); - if (btn) { btn.disabled = true; } - request("/shop/frontend/after-sale/" + tid + "/return-shipping", { - carrier: carrier, - tracking_no: tracking - }).then(function (result) { - if (btn) btn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "提交失败"); - return; - } - closeReturnShipModal(); - showToast(result.message || "退货物流已提交"); - window.location.reload(); - }).catch(function () { - if (btn) btn.disabled = false; - showToast("网络错误"); - }); - }); - } - /* ── Coupon wallet ── */ var couponRoot = page.querySelector("[data-aksrt-coupon-wallet-list]"); if (couponRoot) { diff --git a/assets/js/product-archive.js b/assets/js/product-archive.js deleted file mode 100644 index 7f387b44..00000000 --- a/assets/js/product-archive.js +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Product archive — cat filter + sort + AJAX load more (zibll ajax-next UX). - * - * Uses REST /shop/frontend/products/archive and optional AksrtAjaxList events. - * - * @package AKSRT - */ -(function () { - "use strict"; - - var root = document.querySelector("[data-aksrt-product-archive]"); - if (!root) return; - - function restBase() { - var site = window.aksrtData && aksrtData.siteUrl ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; - return site + "/wp-json/aksrt/v1"; - } - - function toast(msg) { - if (typeof window.aksrtToast === "function") { - window.aksrtToast(msg); - return; - } - if (window.console) console.log(msg); - } - - var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid"); - var pager = root.querySelector("[data-aksrt-ajaxpager], .ajaxpager"); - var countEl = root.querySelector("[data-aksrt-archive-count]"); - var headerHost = root.querySelector("[data-aksrt-ajax-header]"); - var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); - var loader = root.querySelector("[data-aksrt-ajax-loader], .post_ajax_loader"); - - var state = { - taxonomy: root.getAttribute("data-taxonomy") || "", - termId: Number(root.getAttribute("data-term-id") || 0), - orderby: root.getAttribute("data-orderby") || "date", - order: (root.getAttribute("data-order") || "DESC").toUpperCase(), - page: Number(root.getAttribute("data-page") || 1), - maxPages: Number(root.getAttribute("data-max-pages") || 1), - paging: root.getAttribute("data-paging") || "number", - ias: root.getAttribute("data-ias") === "1", - iasMax: Number(root.getAttribute("data-ias-max") || 5), - loading: false, - autoLoads: 0, - }; - - function showLoader(on) { - if (loader) { - loader.hidden = !on; - loader.style.display = on ? "" : "none"; - loader.classList.toggle("is-loading", !!on); - } - if (pager) pager.classList.toggle("is-ajax-loading", !!on); - } - - function updateLoadMore(hasMore) { - var pag = root.querySelector(".ajax-pag"); - if (loadMoreBtn) { - loadMoreBtn.style.display = hasMore ? "" : "none"; - loadMoreBtn.disabled = false; - loadMoreBtn.textContent = hasMore ? "加载更多" : "没有更多了"; - if (!hasMore) loadMoreBtn.classList.add("ajax-nomore"); - else loadMoreBtn.classList.remove("ajax-nomore"); - } - if (pag) pag.style.display = hasMore ? "" : "none"; - } - - function syncOrderbyActive() { - root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) { - var key = a.getAttribute("data-aksrt-shop-orderby") || ""; - var active = key === state.orderby; - a.classList.toggle("is-active", active); - a.classList.toggle("focus-color", active && key === "price"); - }); - } - - function fetchArchive(page, orderby, order, replace, termId) { - if (state.loading) return Promise.resolve(); - state.loading = true; - showLoader(true); - if (loadMoreBtn) { - loadMoreBtn.disabled = true; - loadMoreBtn.textContent = "加载中…"; - } - - var qs = new URLSearchParams(); - qs.set("taxonomy", state.taxonomy); - qs.set("term_id", String(typeof termId === "number" ? termId : state.termId)); - qs.set("page", String(page)); - qs.set("orderby", orderby || state.orderby); - if ((orderby || state.orderby) === "price") { - qs.set("order", order || state.order || "ASC"); - } - - var container = pager || grid || root; - try { - container.dispatchEvent( - new CustomEvent("post_ajax.before", { - bubbles: true, - detail: { page: page, orderby: orderby || state.orderby, replace: !!replace }, - }) - ); - } catch (e0) {} - - return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), { - method: "GET", - credentials: "same-origin", - headers: { - Accept: "application/json", - "X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "", - "X-AKSRT-Ajax": "list", - }, - }) - .then(function (r) { - return r.json(); - }) - .then(function (result) { - state.loading = false; - showLoader(false); - if (!result || !result.success || !result.data) { - toast((result && result.message) || "加载失败"); - updateLoadMore(state.page < state.maxPages); - return; - } - var data = result.data; - state.page = Number(data.page || page); - state.maxPages = Number(data.max_pages || 1); - state.orderby = data.orderby || state.orderby; - state.order = String(data.order || state.order || "DESC").toUpperCase(); - if (typeof termId === "number" && termId > 0) { - state.termId = termId; - } - if (data.term && data.term.id) { - state.termId = Number(data.term.id); - } - root.setAttribute("data-page", String(state.page)); - root.setAttribute("data-max-pages", String(state.maxPages)); - root.setAttribute("data-orderby", state.orderby); - root.setAttribute("data-order", state.order); - root.setAttribute("data-term-id", String(state.termId)); - - if (countEl && typeof data.total !== "undefined") { - countEl.textContent = "共 " + data.total + " 件商品"; - } - var titleEl = root.querySelector("[data-aksrt-archive-title]"); - if (titleEl && data.term && data.term.name) { - titleEl.textContent = data.term.name; - } - var descEl = root.querySelector("[data-aksrt-archive-desc]"); - if (descEl && data.term) { - descEl.textContent = data.term.description || ""; - descEl.style.display = data.term.description ? "" : "none"; - } - - if (headerHost && data.header_html) { - // Keep title/count; replace filter widget if present. - var existingFilter = headerHost.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter"); - var tmp = document.createElement("div"); - tmp.innerHTML = data.header_html; - var nextFilter = tmp.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter") || tmp.firstElementChild; - if (existingFilter && nextFilter) { - existingFilter.replaceWith(nextFilter); - } else if (!existingFilter && nextFilter) { - headerHost.appendChild(nextFilter); - } - } - - var html = data.html || data.empty_html || ""; - if (grid) { - if (replace) { - grid.innerHTML = html; - } else if (html) { - grid.insertAdjacentHTML("beforeend", html); - } - } - - if (data.paginate_html) { - var oldPag = root.querySelector(".ajax-pag"); - var wrap = document.createElement("div"); - wrap.innerHTML = data.paginate_html; - var nextPag = wrap.firstElementChild; - if (oldPag && nextPag) oldPag.replaceWith(nextPag); - else if (!oldPag && nextPag && pager) pager.appendChild(nextPag); - loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); - } - - syncOrderbyActive(); - updateLoadMore(!!data.has_more || state.page < state.maxPages); - - try { - container.dispatchEvent( - new CustomEvent("post_ajax.ed", { - bubbles: true, - detail: { page: state.page, replace: !!replace, data: data }, - }) - ); - document.dispatchEvent( - new CustomEvent("aksrt:posts:rendered", { - bubbles: true, - detail: { container: grid, replace: !!replace }, - }) - ); - } catch (e1) {} - }) - .catch(function () { - state.loading = false; - showLoader(false); - updateLoadMore(state.page < state.maxPages); - toast("网络错误"); - }); - } - - function pushUrl(orderby, order, termUrl) { - try { - var u = termUrl ? new URL(termUrl, window.location.origin) : new URL(window.location.href); - if (orderby) u.searchParams.set("orderby", orderby); - else u.searchParams.delete("orderby"); - u.searchParams.delete("shop_orderby"); - if (orderby === "price" && order) u.searchParams.set("order", order); - else u.searchParams.delete("order"); - u.searchParams.delete("paged"); - window.history.replaceState({}, "", u.toString()); - } catch (err) {} - } - - root.addEventListener("click", function (e) { - // Category / sort links with ajax-next — intercept for REST (richer payload). - var termLink = e.target.closest("[data-aksrt-shop-term].ajax-next, a.ajax-next[data-aksrt-shop-term]"); - if (termLink && root.contains(termLink)) { - e.preventDefault(); - e.stopPropagation(); - var tid = Number(termLink.getAttribute("data-aksrt-shop-term") || 0); - var href = termLink.getAttribute("href") || ""; - state.autoLoads = 0; - if (tid > 0) state.termId = tid; - pushUrl(state.orderby, state.order, href); - var rt = termLink.getAttribute("route-title"); - if (rt) { - try { - document.title = rt; - } catch (e2) {} - } - fetchArchive(1, state.orderby, state.order, true, tid); - return; - } - - var sort = e.target.closest("[data-aksrt-shop-orderby]"); - if (sort && root.contains(sort) && sort.tagName === "A") { - e.preventDefault(); - e.stopPropagation(); - var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date"; - var nextOrder = sort.getAttribute("data-aksrt-shop-order") || ""; - if (orderby === "price") { - if (state.orderby === "price" && nextOrder) { - state.order = nextOrder.toUpperCase(); - } else if (state.orderby !== "price") { - state.order = "ASC"; - } else { - state.order = state.order === "ASC" ? "DESC" : "ASC"; - } - } - if (orderby === state.orderby && orderby !== "price" && state.paging === "ajax") return; - state.orderby = orderby; - state.autoLoads = 0; - pushUrl(orderby, state.order); - fetchArchive(1, orderby, state.order, true); - return; - } - - var more = e.target.closest("[data-aksrt-shop-load-more]"); - if (more && root.contains(more)) { - e.preventDefault(); - e.stopPropagation(); - if (state.page >= state.maxPages) return; - fetchArchive(state.page + 1, state.orderby, state.order, false); - } - }); - - if (state.paging === "ajax" && state.ias && "IntersectionObserver" in window && loadMoreBtn) { - var io = new IntersectionObserver( - function (entries) { - entries.forEach(function (entry) { - if (!entry.isIntersecting) return; - if (state.loading || state.page >= state.maxPages) return; - if (state.iasMax > 0 && state.autoLoads >= state.iasMax) return; - state.autoLoads += 1; - fetchArchive(state.page + 1, state.orderby, state.order, false); - }); - }, - { rootMargin: "240px" } - ); - io.observe(loadMoreBtn); - } -})(); diff --git a/assets/js/product-purchase.js b/assets/js/product-purchase.js deleted file mode 100644 index 9dea3012..00000000 --- a/assets/js/product-purchase.js +++ /dev/null @@ -1,407 +0,0 @@ -/** - * Product purchase page — options selector + server cart. - * - * @package AKSRT - */ -(function () { - "use strict"; - - function restBase() { - var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; - return root + "/wp-json/aksrt/v1"; - } - - function restNonce() { - return (window.aksrtData && aksrtData.restNonce) || ""; - } - - function toast(msg, type) { - if (typeof window.aksrtToast === "function") { - window.aksrtToast(msg, type || "info"); - return; - } - if (window.aksrt && typeof aksrt.notyf === "function") { - aksrt.notyf(msg, { type: type || "info" }); - return; - } - window.alert(msg); - } - - function rest(path, method, body) { - var opts = { - method: method || "GET", - credentials: "same-origin", - headers: { - "X-WP-Nonce": restNonce(), - }, - }; - if (body !== undefined) { - opts.headers["Content-Type"] = "application/json"; - opts.body = JSON.stringify(body); - } - return fetch(restBase() + path, opts).then(function (r) { - return r.json(); - }); - } - - function money(value, payModo) { - var n = Number(value || 0).toFixed(2); - return payModo === "points" ? n : "¥" + n; - } - - /* ── Product page ── */ - var productPage = document.querySelector("[data-aksrt-product-page]"); - if (productPage) { - var quantityInput = productPage.querySelector("[data-aksrt-quantity]"); - var productId = Number(productPage.getAttribute("data-product-id") || 0); - var titleEl = productPage.querySelector(".aksrt-product-page__title"); - var imageEl = productPage.querySelector(".aksrt-product-page__primary-image img"); - var priceEl = productPage.querySelector("[data-aksrt-dynamic-price], .aksrt-product-page__price"); - var stockEl = productPage.querySelector("[data-aksrt-stock-display]"); - var couponEl = productPage.querySelector("[data-aksrt-product-coupon]"); - var optionsRoot = productPage.querySelector("[data-aksrt-options-selector]"); - var optionResult = productPage.querySelector("[data-aksrt-option-result]"); - var selectedLabelsEl = productPage.querySelector("[data-aksrt-selected-labels]"); - var resolvedPriceEl = productPage.querySelector("[data-aksrt-resolved-price]"); - var resolvedStockEl = productPage.querySelector("[data-aksrt-resolved-stock]"); - var payModo = productPage.getAttribute("data-pay-modo") || "0"; - var basePrice = Number(productPage.getAttribute("data-base-price") || 0); - var baseStock = Number(productPage.getAttribute("data-base-stock") || 0); - var selectedByGroup = {}; - var groupCount = optionsRoot - ? optionsRoot.querySelectorAll(".aksrt-product-page__option-group").length - : 0; - - function currentOptionsKey() { - if (!groupCount) return ""; - var labels = []; - for (var i = 0; i < groupCount; i++) { - if (!selectedByGroup[i]) return null; - labels.push(selectedByGroup[i].label); - } - return labels.join("|"); - } - - function resolveSelection() { - if (!groupCount) { - return { - key: "", - labels: "", - price: basePrice, - stock: baseStock, - image: "", - }; - } - var key = currentOptionsKey(); - if (key === null) return null; - - var price = basePrice; - var image = ""; - var labels = []; - for (var i = 0; i < groupCount; i++) { - var sel = selectedByGroup[i]; - labels.push(sel.label); - price += Number(sel.priceChange || 0); - if (sel.image) image = sel.image; - } - return { - key: key, - labels: labels.join(" / "), - price: Math.max(0, price), - stock: baseStock, - image: image, - }; - } - - function paintSelection() { - var resolved = resolveSelection(); - if (!resolved) { - if (optionResult) optionResult.hidden = true; - return; - } - if (priceEl) { - if (groupCount && productPage.getAttribute("data-price-range") === "1" && currentOptionsKey() === null) { - /* keep range until complete selection */ - } else { - priceEl.textContent = money(resolved.price, payModo) + (payModo === "points" ? "" : ""); - if (payModo === "points" && priceEl.querySelector("small") === null) { - /* leave as-is */ - } - } - } - if (stockEl && resolved.stock > -1) { - stockEl.textContent = String(resolved.stock); - } - if (quantityInput && resolved.stock > 0) { - quantityInput.setAttribute("max", String(resolved.stock)); - if (Number(quantityInput.value) > resolved.stock) { - quantityInput.value = String(resolved.stock); - } - } - if (imageEl && resolved.image) { - imageEl.setAttribute("src", resolved.image); - } - if (optionResult) { - optionResult.hidden = false; - if (selectedLabelsEl) selectedLabelsEl.textContent = resolved.labels; - if (resolvedPriceEl) resolvedPriceEl.textContent = money(resolved.price, payModo); - if (resolvedStockEl) { - resolvedStockEl.textContent = - resolved.stock <= -1 ? "库存充足" : "库存 " + resolved.stock; - } - } - } - - if (optionsRoot) { - optionsRoot.addEventListener("click", function (event) { - var btn = event.target.closest("[data-option-group]"); - if (!btn || !optionsRoot.contains(btn)) return; - var group = btn.getAttribute("data-option-group"); - var label = btn.getAttribute("data-option-label") || ""; - var priceChange = Number(btn.getAttribute("data-price-change") || 0); - var image = btn.getAttribute("data-option-image") || ""; - - optionsRoot - .querySelectorAll('[data-option-group="' + group + '"]') - .forEach(function (el) { - el.classList.remove("is-active"); - }); - btn.classList.add("is-active"); - selectedByGroup[group] = { label: label, priceChange: priceChange, image: image }; - paintSelection(); - }); - } - - function ensureOptionsSelected() { - if (!groupCount) return true; - if (currentOptionsKey() === null) { - toast("请先选择商品规格", "warning"); - return false; - } - return true; - } - - function addToServerCart(replaceQty) { - if (!window.aksrtData || !aksrtData.isLoggedIn) { - window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; - return; - } - if (!ensureOptionsSelected()) return; - - var qty = Number(quantityInput ? quantityInput.value : 1) || 1; - var optionsString = currentOptionsKey() || ""; - var body = { - product_id: productId, - options_string: optionsString, - count: qty, - }; - - rest("/shop/frontend/cart/add", "POST", body) - .then(function (result) { - if (!result || !result.success) { - toast((result && (result.message || result.error)) || "加入购物车失败", "error"); - return; - } - toast("已加入购物车", "success"); - if (replaceQty) { - var cartUrl = new URL( - (window.aksrtData && (aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart))) || "/", - window.location.origin - ); - if (couponEl && couponEl.value) { - cartUrl.searchParams.set("coupon", couponEl.value); - } - window.location.href = cartUrl.toString(); - } - }) - .catch(function () { - toast("网络错误,请稍后重试", "error"); - }); - } - - productPage.addEventListener("click", function (event) { - var qtyBtn = event.target.closest("[data-aksrt-qty-action]"); - if (qtyBtn && quantityInput) { - var action = qtyBtn.getAttribute("data-aksrt-qty-action"); - var current = Number(quantityInput.value || 1); - var max = Number(quantityInput.getAttribute("max") || 999); - quantityInput.value = action === "inc" ? Math.min(max, current + 1) : Math.max(1, current - 1); - return; - } - - if (event.target.closest("[data-aksrt-add-cart]")) { - addToServerCart(false); - return; - } - - if (event.target.closest("[data-aksrt-buy-now]")) { - addToServerCart(true); - return; - } - - var thumb = event.target.closest("[data-aksrt-product-thumb]"); - if (thumb && imageEl) { - imageEl.setAttribute("src", thumb.getAttribute("data-aksrt-product-thumb")); - return; - } - - var videoBtn = event.target.closest("[data-aksrt-product-video]"); - if (videoBtn) { - var url = videoBtn.getAttribute("data-aksrt-product-video"); - if (url) window.open(url, "_blank", "noopener"); - return; - } - - var favBtn = event.target.closest("[data-aksrt-toggle-fav]"); - if (favBtn) { - if (!window.aksrtData || !aksrtData.isLoggedIn) { - window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; - return; - } - var pid = Number(favBtn.getAttribute("data-aksrt-toggle-fav") || productId); - rest("/shop/frontend/products/" + pid + "/favorite", "POST", {}) - .then(function (result) { - if (!result || !result.success) { - toast((result && result.message) || "操作失败", "error"); - return; - } - var active = !!(result.data && result.data.is_favorite); - productPage.querySelectorAll("[data-aksrt-toggle-fav]").forEach(function (btn) { - btn.classList.toggle("is-active", active); - }); - toast((result.message) || (active ? "已收藏" : "已取消收藏"), "success"); - }) - .catch(function () { - toast("网络错误", "error"); - }); - return; - } - - var tabBtn = event.target.closest("[data-aksrt-product-tab]"); - if (tabBtn) { - var tab = tabBtn.getAttribute("data-aksrt-product-tab"); - productPage.querySelectorAll("[data-aksrt-product-tab]").forEach(function (b) { - b.classList.toggle("is-active", b === tabBtn); - }); - productPage.querySelectorAll("[data-aksrt-product-panel]").forEach(function (panel) { - panel.hidden = panel.getAttribute("data-aksrt-product-panel") !== tab; - }); - return; - } - - if (event.target.closest("[data-aksrt-show-params]")) { - var paramsModal = productPage.querySelector("[data-aksrt-params-modal]"); - if (paramsModal) paramsModal.hidden = false; - return; - } - if (event.target.closest("[data-aksrt-show-services]")) { - var servicesModal = productPage.querySelector("[data-aksrt-services-modal]"); - if (servicesModal) servicesModal.hidden = false; - return; - } - if (event.target.closest("[data-aksrt-close-modal]")) { - var modal = event.target.closest("[data-aksrt-params-modal], [data-aksrt-services-modal], .aksrt-modal"); - if (modal) modal.hidden = true; - } - }); - } - - /* ── Product editor (seller) ── */ - var editor = document.querySelector("[data-aksrt-product-editor]"); - if (!editor || !window.aksrtData) { - return; - } - - var form = editor.querySelector("[data-aksrt-product-form]"); - var messageEl = editor.querySelector("[data-aksrt-product-editor-message]"); - if (!form) return; - - function selectedMultiValues(select) { - if (!select) return []; - return Array.prototype.slice - .call(select.options) - .filter(function (option) { return option.selected; }) - .map(function (option) { return Number(option.value || 0); }) - .filter(Boolean); - } - - function parseOptionsText(text) { - var groups = []; - String(text || "").split(String.fromCharCode(10)).forEach(function (line) { - line = line.replace(/\r/g, "").trim(); - if (!line) return; - var parts = line.split("|"); - if (parts.length < 2) return; - var name = parts[0].trim(); - var values = []; - parts.slice(1).join("|").split(",").forEach(function (chunk) { - chunk = chunk.trim(); - if (!chunk) return; - var segs = chunk.split(":"); - var label = (segs[0] || "").trim(); - if (!label) return; - values.push({ label: label, price_change: Number(segs[1] || 0), image: "" }); - }); - if (name && values.length) { - groups.push({ name: name, view_mode: "list", values: values }); - } - }); - return groups; - } - - form.addEventListener("submit", function (event) { - event.preventDefault(); - - var formData = new FormData(form); - var productId = Number(editor.getAttribute("data-product-id") || 0); - var gallery = String(formData.get("gallery") || "") - .split(String.fromCharCode(10)) - .map(function (line) { return line.replace(/\r/g, "").trim(); }) - .filter(Boolean); - - var payload = { - title: String(formData.get("title") || "").trim(), - price: Number(formData.get("price") || 0), - stock: Number(formData.get("stock") || 0), - excerpt: String(formData.get("excerpt") || ""), - content: String(formData.get("content") || ""), - status: String(formData.get("status") || "draft"), - categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')), - tags: String(formData.get("tags") || "") - .split(",") - .map(function (tag) { return tag.trim(); }) - .filter(Boolean), - image: String(formData.get("image") || "").trim(), - gallery: gallery, - shipping_mode: String(formData.get("shipping_mode") || "express"), - shipping_fee: Number(formData.get("shipping_fee") || 0), - after_sale_policy: String(formData.get("after_sale_policy") || ""), - support_refund: formData.get("support_refund") === "1", - options: parseOptionsText(formData.get("options_text") || ""), - start_price: Number(formData.get("start_price") || 0), - pay_modo: String(formData.get("pay_modo") || "0"), - product_params: String(formData.get("product_params") || ""), - product_video: String(formData.get("product_video") || "") - .split(String.fromCharCode(10)) - .map(function (line) { return line.replace(/\r/g, "").trim(); }) - .filter(Boolean) - }; - - var path = productId > 0 ? "/shop/frontend/products/" + productId : "/shop/frontend/products"; - if (messageEl) messageEl.textContent = "正在保存商品..."; - - rest(path, "POST", payload) - .then(function (result) { - if (!result.success) { - if (messageEl) messageEl.textContent = result.message || "保存失败。"; - return; - } - var centerUrl = aksrtData.userCenterUrl || "/"; - if (messageEl) messageEl.textContent = "商品已保存,正在返回用户中心..."; - window.location.href = centerUrl; - }) - .catch(function () { - if (messageEl) messageEl.textContent = "网络错误,请稍后重试。"; - }); - }); -})(); diff --git a/assets/js/widget-set.js b/assets/js/widget-set.js index eab96e74..a3d68811 100644 --- a/assets/js/widget-set.js +++ b/assets/js/widget-set.js @@ -156,73 +156,6 @@ }); } - // Forum multi-tab topics. - function initForumTabTopics() { - document.querySelectorAll('.aksrt-ftab[data-aksrt-ftab]').forEach(function (root) { - if (root.dataset.aksrtFtabBound === '1') return; - root.dataset.aksrtFtabBound = '1'; - var rest = root.getAttribute('data-rest') || ''; - var count = root.getAttribute('data-count') || '6'; - var style = root.getAttribute('data-style') || 'mini'; - var plate = root.getAttribute('data-plate') || '0'; - var btns = [].slice.call(root.querySelectorAll('.aksrt-ftab__btn')); - var panels = [].slice.call(root.querySelectorAll('.aksrt-ftab__panel')); - - function load(panel, orderby) { - if (!panel || panel.getAttribute('data-loaded') === '1' || !rest) return; - var qs = new URLSearchParams({ - page: '1', - count: count, - orderby: orderby || 'date', - style: style, - plate: plate, - period: 'all', - paginate: 'none', - }); - var url = rest + (rest.indexOf('?') > -1 ? '&' : '?') + qs.toString(); - panel.classList.add('is-ajax-loading'); - fetch(url, { credentials: 'same-origin', headers: restHeaders() }) - .then(function (r) { return r.json(); }) - .then(function (j) { - var d = (j && j.data) || j || {}; - var html = d.html || ''; - panel.innerHTML = html || '
暂无内容
'; - panel.setAttribute('data-loaded', '1'); - panel.classList.remove('is-ajax-loading'); - }) - .catch(function () { - panel.classList.remove('is-ajax-loading'); - }); - } - - function go(i) { - btns.forEach(function (b, idx) { - var on = idx === i; - b.classList.toggle('is-active', on); - b.setAttribute('aria-selected', on ? 'true' : 'false'); - }); - panels.forEach(function (p, idx) { - var on = idx === i; - p.classList.toggle('is-active', on); - if (on) { - p.removeAttribute('hidden'); - } else { - p.setAttribute('hidden', ''); - } - }); - var btn = btns[i]; - var panel = panels[i]; - if (btn && panel) load(panel, btn.getAttribute('data-orderby') || 'date'); - } - - btns.forEach(function (b) { - b.addEventListener('click', function () { - go(parseInt(b.getAttribute('data-index') || '0', 10)); - }); - }); - }); - } - // Product multi-tab. function initProductTabs() { document.querySelectorAll('[data-aksrt-product-tab]').forEach(function (root) { @@ -383,86 +316,6 @@ } - function initModeratorApplyModal() { - document.addEventListener('click', function (e) { - var openBtn = e.target.closest('[data-aksrt-mod-apply-open]'); - if (openBtn) { - e.preventDefault(); - var id = openBtn.getAttribute('data-aksrt-mod-apply-open'); - var modal = id ? document.getElementById(id) : null; - if (modal) { - modal.hidden = false; - document.body.classList.add('aksrt-mod-apply-open'); - } - return; - } - var closeBtn = e.target.closest('[data-aksrt-mod-apply-close]'); - if (closeBtn) { - var box = closeBtn.closest('.aksrt-mod-apply-modal'); - if (box) { - box.hidden = true; - document.body.classList.remove('aksrt-mod-apply-open'); - } - } - }); - - document.querySelectorAll('.js-aksrt-moderator-apply-form').forEach(function (form) { - if (form.dataset.aksrtModApplyBound === '1') return; - form.dataset.aksrtModApplyBound = '1'; - form.addEventListener('submit', function (event) { - event.preventDefault(); - var boardId = parseInt(form.getAttribute('data-board-id') || '0', 10); - var reasonField = form.querySelector('[name="reason"]'); - var reason = reasonField ? reasonField.value.trim() : ''; - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - if (!boardId || !reason) { - if (feedback) { - feedback.textContent = '请填写完整的申请理由。'; - feedback.className = 'js-aksrt-action-feedback mb10 is-error'; - } - return; - } - if (submitButton) submitButton.disabled = true; - if (feedback) { - feedback.textContent = '正在提交申请…'; - feedback.className = 'js-aksrt-action-feedback mb10 is-loading'; - } - var siteUrl = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/+$/, '') : ''; - var url = siteUrl + '/wp-json/aksrt/v1/forum/moderator/apply'; - fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-WP-Nonce': (window.aksrtData && aksrtData.restNonce) || '', - }, - body: JSON.stringify({ board_id: boardId, reason: reason }), - }) - .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) - .then(function (res) { - if (!res.ok || (res.j && res.j.success === false)) { - throw new Error((res.j && res.j.message) || '申请提交失败'); - } - var msg = (res.j && (res.j.message || (res.j.data && res.j.data.message))) || '申请已提交'; - if (feedback) { - feedback.textContent = msg; - feedback.className = 'js-aksrt-action-feedback mb10 is-success'; - } - if (reasonField) reasonField.disabled = true; - }) - .catch(function (err) { - if (feedback) { - feedback.textContent = (err && err.message) || '申请提交失败。'; - feedback.className = 'js-aksrt-action-feedback mb10 is-error'; - } - if (submitButton) submitButton.disabled = false; - }); - }); - }); - } - function initFloatingQRCodes() { var wrap = document.querySelector('.float-right'); if (!wrap) return; @@ -508,11 +361,9 @@ function init() { initWidgetTabs(); initCategoryTabs(); - initForumTabTopics(); initProductTabs(); initMainPostsLazy(); initFeatureIntro(); - initModeratorApplyModal(); initFloatingQRCodes(); } diff --git a/footer.php b/footer.php index 8778740d..84ee0e82 100644 --- a/footer.php +++ b/footer.php @@ -396,12 +396,6 @@ if ($floating_enabled) : - 'fas fa-shopping-cart']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } ?> diff --git a/functions.php b/functions.php index c897bb0b..ff6b293a 100644 --- a/functions.php +++ b/functions.php @@ -111,7 +111,7 @@ add_filter('query_vars', function ($vars) { // 主题切换离开时:清理本主题注册的 cron 任务 add_action('switch_theme', function () { - $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup', 'aksrt_reconcile_refunds', 'aksrt_after_sale_auto_cancel']; + $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup']; foreach ($hooks as $hook) { $next = wp_next_scheduled($hook); while ($next) { @@ -211,12 +211,6 @@ if (class_exists('\AKSRT\Content\Download')) { if (class_exists('\AKSRT\Content\Pay')) { \AKSRT\Content\Pay::get_instance()->init(); } -if (class_exists('\AKSRT\Forum\TopicPay')) { - \AKSRT\Forum\TopicPay::get_instance()->init(); -} -if (class_exists('\AKSRT\Forum\PlateFollowPay')) { - \AKSRT\Forum\PlateFollowPay::get_instance()->init(); -} if (class_exists('\AKSRT\Content\VipService')) { \AKSRT\Content\VipService::get_instance()->init(); } @@ -324,10 +318,6 @@ if (class_exists('\AKSRT\Payment\Order')) { \AKSRT\Payment\Order::get_instance()->init(); } -if (class_exists('\AKSRT\Payment\AfterSale')) { - \AKSRT\Payment\AfterSale::get_instance()->init(); -} - // Register built-in payment gateways if (class_exists('\AKSRT\Payment\Gateway')) { $gw = \AKSRT\Payment\Gateway::get_instance(); @@ -355,10 +345,7 @@ if (class_exists('\AKSRT\Payment\Gateway')) { do_action('aksrt_register_payment_gateways', $gw); } -if (class_exists('\AKSRT\Forum\Forum')) { - \AKSRT\Forum\Forum::get_instance()->init(); -} -if (is_admin() && class_exists('\AKSRT\Core\ResourceMetaBox')) { +if (class_exists('\AKSRT\Core\ResourceMetaBox')) { \AKSRT\Core\ResourceMetaBox::get_instance()->init(); } @@ -367,51 +354,6 @@ if (class_exists('\AKSRT\Core\TermMeta')) { \AKSRT\Core\TermMeta::get_instance()->init(); } -// 商城 taxonomy term meta + shop_discount 优惠活动 -if (class_exists('\AKSRT\Shop\TermMeta')) { - \AKSRT\Shop\TermMeta::get_instance()->init(); -} - -// 商品后台列表自定义列(销量/综合数据/商家/优惠活动) -if (is_admin() && class_exists('\AKSRT\Admin\ShopController')) { - \AKSRT\Admin\ShopController::get_instance()->init_product_admin_columns(); -} - -// 商品原生编辑 meta box(价格/库存/发货售后) -if (is_admin() && class_exists('\AKSRT\Shop\ProductMetaBox')) { - \AKSRT\Shop\ProductMetaBox::get_instance()->init(); -} -// 商品原生变体 meta box -if (is_admin() && class_exists('\AKSRT\Shop\ProductVariantsMetaBox')) { - \AKSRT\Shop\ProductVariantsMetaBox::get_instance()->init(); -} - -// 论坛版块版主 meta box -if (is_admin() && class_exists('\AKSRT\Forum\PlateMetaBox')) { - \AKSRT\Forum\PlateMetaBox::get_instance()->init(); -} -// 论坛帖子原生列表治理列/筛选/批量 -if (is_admin() && class_exists('\AKSRT\Forum\TopicAdminList')) { - \AKSRT\Forum\TopicAdminList::get_instance()->init(); -} -// 论坛帖子治理 meta box -if (is_admin() && class_exists('\AKSRT\Forum\TopicMetaBox')) { - \AKSRT\Forum\TopicMetaBox::get_instance()->init(); -} -// 论坛版块列表列 -if (is_admin() && class_exists('\AKSRT\Forum\PlateAdminList')) { - \AKSRT\Forum\PlateAdminList::get_instance()->init(); -} - - - - - -// 商品评价/评分系统 -if (class_exists('\AKSRT\Shop\ProductReview')) { - \AKSRT\Shop\ProductReview::get_instance()->init(); -} - // 文章局部配置 meta box(仅后台) if (is_admin() && class_exists('\AKSRT\Core\PostMetaBox')) { \AKSRT\Core\PostMetaBox::get_instance()->init(); @@ -464,12 +406,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayCoupons')) { if (is_admin() && class_exists('\AKSRT\Admin\Native\PayChargeCards')) { \AKSRT\Admin\Native\PayChargeCards::get_instance()->init(); } -if (is_admin() && class_exists('\AKSRT\Admin\Native\PayShipping')) { - \AKSRT\Admin\Native\PayShipping::get_instance()->init(); -} -if (is_admin() && class_exists('\AKSRT\Admin\Native\PayAfterSale')) { - \AKSRT\Admin\Native\PayAfterSale::get_instance()->init(); -} if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) { \AKSRT\Admin\Native\PayOrders::get_instance()->init(); } diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 10e8ee03..eaac1682 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -38,7 +38,6 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-comments.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-forum.php'; -require_once AKSRT_DIR . '/inc/helpers/aksrt-forum-widgets.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-slider.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-vip.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-auth-ui.php'; diff --git a/inc/core-hooks/part-1.php b/inc/core-hooks/part-1.php index 98dfa457..2f821276 100644 --- a/inc/core-hooks/part-1.php +++ b/inc/core-hooks/part-1.php @@ -83,339 +83,4 @@ function aksrt_register_topics_taxonomy() { } add_action('init', 'aksrt_register_topics_taxonomy'); -// ============================================================ -// 注册自定义文章类型 - Forum Topic & 分类法 - Forum Board -// ============================================================ - -function aksrt_register_forum_cpt() { - // Respect bbs_s switch — skip CPT registration when forum is disabled. - // Admin context always registers so the backend settings page works. - if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - if (empty($forum['enabled'])) { - return; - } - } - - // Read custom slugs / names from settings when available - $plate_slug = 'board'; - $topic_slug = 'topic'; - $forum_enabled = true; - $forum_settings = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum_settings = \AKSRT\Settings\Settings::get_forum_settings(); - $plate_slug = !empty($forum_settings['other']['plate_slug']) ? $forum_settings['other']['plate_slug'] : 'board'; - $topic_slug = !empty($forum_settings['other']['posts_slug']) ? $forum_settings['other']['posts_slug'] : 'topic'; - $forum_enabled = !empty($forum_settings['enabled']); - } - // Admin always registers types; menu follows bbs_s (zibll: show when forum on). - $forum_show_menu = is_admin() ? $forum_enabled : false; - $names = !empty($forum_settings['names']) ? $forum_settings['names'] : []; - $forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt'))); - $posts_label = sanitize_text_field((string) ($names['posts'] ?? __('帖子', 'aksrt'))); - $topic_label = sanitize_text_field((string) ($names['topic'] ?? __('话题', 'aksrt'))); - $tag_label = sanitize_text_field((string) ($names['tag'] ?? __('标签', 'aksrt'))); - // Legacy board taxonomy: keep rewrite/bridge, hide from admin (plate CPT is canonical). - register_taxonomy('aksrt_forum_board', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => __('论坛版块', 'aksrt'), - 'singular_name' => __('版块', 'aksrt'), - 'search_items' => __('搜索版块', 'aksrt'), - 'all_items' => __('所有版块', 'aksrt'), - 'edit_item' => __('编辑版块', 'aksrt'), - 'add_new_item' => __('添加版块', 'aksrt'), - ], - 'hierarchical' => true, - 'public' => true, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_rest' => true, - 'show_admin_column' => false, - 'rewrite' => ['slug' => $plate_slug], - ]); - - // Forum Tag taxonomy - register_taxonomy('aksrt_forum_tag', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: forum name */ __('%s标签', 'aksrt'), $forum_label), - 'singular_name' => $tag_label, - 'search_items' => sprintf(/* translators: %s: tag name */ __('搜索%s', 'aksrt'), $tag_label), - 'all_items' => sprintf(/* translators: %s: tag name */ __('所有%s', 'aksrt'), $tag_label), - 'edit_item' => sprintf(/* translators: %s: tag name */ __('编辑%s', 'aksrt'), $tag_label), - 'add_new_item' => sprintf(/* translators: %s: tag name */ __('添加%s', 'aksrt'), $tag_label), - 'menu_name' => $tag_label, - ], - 'hierarchical' => false, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'forum-tag'], - ]); - - // Forum Topic taxonomy (话题,单选,区别于标签) - register_taxonomy('aksrt_forum_topic_cat', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: forum name */ __('%s话题', 'aksrt'), $forum_label), - 'singular_name' => $topic_label, - 'search_items' => sprintf(/* translators: %s: topic name */ __('搜索%s', 'aksrt'), $topic_label), - 'all_items' => sprintf(/* translators: %s: topic name */ __('所有%s', 'aksrt'), $topic_label), - 'edit_item' => sprintf(/* translators: %s: topic name */ __('编辑%s', 'aksrt'), $topic_label), - 'add_new_item' => sprintf(/* translators: %s: topic name */ __('添加%s', 'aksrt'), $topic_label), - 'menu_name' => $topic_label, - ], - 'hierarchical' => false, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'forum-topic'], - ]); - - // Forum Topic CPT — native sidebar (zibll: forum_post @ pos 40) - register_post_type('aksrt_forum_topic', [ - 'labels' => [ - 'name' => $forum_label . $posts_label, - 'singular_name' => $posts_label, - 'add_new' => sprintf(/* translators: %s: posts name */ __('添加%s', 'aksrt'), $posts_label), - 'add_new_item' => sprintf(/* translators: %s: posts name */ __('发布新%s', 'aksrt'), $posts_label), - 'edit_item' => sprintf(/* translators: %s: posts name */ __('编辑%s', 'aksrt'), $posts_label), - 'view_item' => sprintf(/* translators: %s: posts name */ __('查看%s', 'aksrt'), $posts_label), - 'all_items' => sprintf(/* translators: %s: posts name */ __('所有%s', 'aksrt'), $posts_label), - 'menu_name' => $forum_label . $posts_label, - ], - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'has_archive' => true, - 'menu_position' => 40, - 'menu_icon' => 'dashicons-buddicons-topics', - 'supports' => ['title', 'editor', 'author', 'comments', 'thumbnail', 'excerpt'], - 'capability_type' => 'post', - 'rewrite' => ['slug' => $topic_slug], - ]); -} -add_action('init', 'aksrt_register_forum_cpt'); - -// ============================================================ -// 注册板块 CPT (aksrt_forum_plate) + 板块分区 taxonomy (aksrt_plate_cat) -// §7.0 plate CPT 脚手架:板块从 taxonomy 迁移为独立 CPT, -// 支持作者(超级版主)、正文/摘要/封面、层级父子、独立 meta 扩展位。 -// ============================================================ - -function aksrt_register_forum_plate_cpt() { - if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - if (empty($forum['enabled'])) { - return; - } - } - - $plate_slug = 'board'; - $forum_enabled = true; - $forum = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $plate_slug = !empty($forum['other']['plate_slug']) ? $forum['other']['plate_slug'] : 'board'; - $forum_enabled = !empty($forum['enabled']); - } - $forum_show_menu = is_admin() ? $forum_enabled : false; - $names = !empty($forum['names']) ? $forum['names'] : []; - $forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt'))); - $plate_label = sanitize_text_field((string) ($names['plate'] ?? __('版块', 'aksrt'))); - - // Plate category taxonomy (分区) - register_taxonomy('aksrt_plate_cat', 'aksrt_forum_plate', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: plate name */ __('%s分区', 'aksrt'), $plate_label), - 'singular_name' => __('分区', 'aksrt'), - 'search_items' => __('搜索分区', 'aksrt'), - 'all_items' => __('所有分区', 'aksrt'), - 'edit_item' => __('编辑分区', 'aksrt'), - 'add_new_item' => __('添加分区', 'aksrt'), - 'menu_name' => __('分区', 'aksrt'), - ], - 'hierarchical' => true, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'plate-cat'], - ]); - - // Plate CPT (板块) — native sidebar (zibll: plate @ pos 40) - register_post_type('aksrt_forum_plate', [ - 'labels' => [ - 'name' => $forum_label . $plate_label, - 'singular_name' => $plate_label, - 'add_new' => sprintf(/* translators: %s: plate name */ __('添加%s', 'aksrt'), $plate_label), - 'add_new_item' => sprintf(/* translators: %s: plate name */ __('创建%s', 'aksrt'), $plate_label), - 'edit_item' => sprintf(/* translators: %s: plate name */ __('编辑%s', 'aksrt'), $plate_label), - 'view_item' => sprintf(/* translators: %s: plate name */ __('查看%s', 'aksrt'), $plate_label), - 'search_items' => sprintf(/* translators: %s: plate name */ __('搜索%s', 'aksrt'), $plate_label), - 'not_found' => sprintf(/* translators: %s: plate name */ __('未找到%s', 'aksrt'), $plate_label), - 'not_found_in_trash' => sprintf(/* translators: %s: plate name */ __('回收站中未找到%s', 'aksrt'), $plate_label), - 'all_items' => sprintf(/* translators: %s: plate name */ __('所有%s', 'aksrt'), $plate_label), - 'menu_name' => $forum_label . $plate_label, - ], - 'public' => true, - 'hierarchical' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'has_archive' => false, - 'menu_position' => 40, - 'menu_icon' => 'dashicons-buddicons-forums', - 'supports' => ['title', 'editor', 'author', 'excerpt', 'thumbnail', 'page-attributes'], - 'capability_type' => 'post', - 'rewrite' => ['slug' => $plate_slug, 'with_front' => false], - ]); - - // Keep aksrt_forum_topic assignable to plates via taxonomy bridge - register_taxonomy_for_object_type('aksrt_forum_board', 'aksrt_forum_topic'); -} -add_action('init', 'aksrt_register_forum_plate_cpt'); - -// ============================================================ -// §7.0 forum_post CPT 对齐:注册帖子 meta 扩展位地基 -// 为 aksrt_forum_topic 注册全部 meta key,提供类型安全 + REST 暴露。 -// ============================================================ - -function aksrt_register_forum_topic_meta() { - $post_type = 'aksrt_forum_topic'; - - $meta_fields = [ - // 基础治理 meta (已有,补注册) - '_aksrt_visibility' => ['type' => 'string', 'default' => 'public'], - '_aksrt_review_status' => ['type' => 'string', 'default' => ''], - '_aksrt_sticky' => ['type' => 'integer', 'default' => 0], - '_aksrt_featured' => ['type' => 'integer', 'default' => 0], - '_aksrt_locked' => ['type' => 'integer', 'default' => 0], - '_aksrt_highlight' => ['type' => 'string', 'default' => ''], - '_aksrt_views' => ['type' => 'integer', 'default' => 0], - '_aksrt_moderation_note' => ['type' => 'string', 'default' => ''], - '_aksrt_moderated_by' => ['type' => 'integer', 'default' => 0], - '_aksrt_moderated_at' => ['type' => 'string', 'default' => ''], - - // 帖子类型 + 问答状态 (Wave3) - '_aksrt_bbs_type' => ['type' => 'string', 'default' => 'standard'], - '_aksrt_question_status' => ['type' => 'integer', 'default' => 0], - '_aksrt_adopted_comment' => ['type' => 'integer', 'default' => 0], - - // 投票 (已有,补注册) - '_aksrt_vote_enabled' => ['type' => 'integer', 'default' => 0], - '_aksrt_vote_title' => ['type' => 'string', 'default' => ''], - '_aksrt_vote_multiple' => ['type' => 'integer', 'default' => 0], - '_aksrt_vote_type' => ['type' => 'string', 'default' => 'standard'], - '_aksrt_vote_expiry_days' => ['type' => 'integer', 'default' => 0], - - // 评分 (Wave3) - '_aksrt_score' => ['type' => 'integer', 'default' => 0], - '_aksrt_recommend_score' => ['type' => 'number', 'default' => 0], - '_aksrt_hot_rewarded' => ['type' => 'integer', 'default' => 0], - - // 收藏 (Wave3) - '_aksrt_favorite_count' => ['type' => 'integer', 'default' => 0], - - // 封面扩展位(论坛帖子封面;前台 forum-topic 渲染) - '_aksrt_cover_type' => ['type' => 'string', 'default' => ''], - '_aksrt_cover_image' => ['type' => 'string', 'default' => ''], - '_aksrt_featured_slide' => ['type' => 'array', 'default' => []], - '_aksrt_featured_video' => ['type' => 'string', 'default' => ''], - '_aksrt_cover_ratio' => ['type' => 'string', 'default' => '16:9'], - - // 付费内容扩展位(论坛帖子付费门槛;前台 forum-topic 渲染) - '_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'], - '_aksrt_pay_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy single VIP - '_aksrt_pay_vip1_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_vip2_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_credits' => ['type' => 'integer', 'default' => 0], // legacy alias of credits_cost - '_aksrt_credits_cost' => ['type' => 'integer', 'default' => 0], // runtime unlock price - '_aksrt_credits_vip1' => ['type' => 'integer', 'default' => 0], - '_aksrt_credits_vip2' => ['type' => 'integer', 'default' => 0], - '_aksrt_pay_hide_part' => ['type' => 'integer', 'default' => 0], - '_aksrt_virtual_sales' => ['type' => 'integer', 'default' => 0], - '_aksrt_topic_coupon_s' => ['type' => 'integer', 'default' => 0], - '_aksrt_visibility_roles' => ['type' => 'array', 'default' => []], - - // 状态工作流扩展位 (Wave4) - '_aksrt_closed' => ['type' => 'integer', 'default' => 0], - '_aksrt_hidden' => ['type' => 'integer', 'default' => 0], - - // 板块 CPT 关联 (§7.0 新增) - '_aksrt_plate_id' => ['type' => 'integer', 'default' => 0], - ]; - - foreach ($meta_fields as $key => $args) { - $schema = [ - 'type' => $args['type'], - 'description' => $key, - 'single' => true, - 'default' => $args['default'], - 'show_in_rest' => false, - ]; - - if ($args['type'] === 'array') { - $schema['show_in_rest'] = false; - $schema['type'] = 'array'; - } - - register_post_meta($post_type, $key, $schema); - } - - // Plate CPT meta registration - $plate_meta = [ - '_aksrt_sort_order' => ['type' => 'integer', 'default' => 0], - '_aksrt_icon' => ['type' => 'string', 'default' => ''], - '_aksrt_moderators' => ['type' => 'array', 'default' => []], - '_aksrt_moderator_roles' => ['type' => 'array', 'default' => []], - '_aksrt_posts_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_reply_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_today_reply_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_plate_views' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_last_reply' => ['type' => 'string', 'default' => ''], - '_aksrt_last_post' => ['type' => 'string', 'default' => ''], - '_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'], - '_aksrt_add_limit' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay_modo' => ['type' => 'string', 'default' => 'money'], - '_aksrt_follow_pay_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_points' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy read fallback → vip1 - '_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_coupon_s' => ['type' => 'integer', 'default' => 0], - ]; - - foreach ($plate_meta as $key => $args) { - register_post_meta('aksrt_forum_plate', $key, [ - 'type' => $args['type'] === 'array' ? 'array' : $args['type'], - 'description' => $key, - 'single' => true, - 'default' => $args['default'], - 'show_in_rest' => false, - ]); - } -} -add_action('init', 'aksrt_register_forum_topic_meta'); - -// Include forum topics in frontend search results (§7.10). -add_action('pre_get_posts', function ($query) { - if (!is_admin() && $query->is_main_query() && $query->is_search()) { - $post_types = $query->get('post_type'); - if (empty($post_types) || $post_types === 'post') { - $query->set('post_type', ['post', 'page', 'aksrt_forum_topic']); - } elseif (is_array($post_types) && !in_array('aksrt_forum_topic', $post_types, true)) { - $post_types[] = 'aksrt_forum_topic'; - $query->set('post_type', $post_types); - } - } -}); - // ============================================================ \ No newline at end of file diff --git a/inc/helpers/aksrt-commerce.php b/inc/helpers/aksrt-commerce.php deleted file mode 100644 index 228f3b1e..00000000 --- a/inc/helpers/aksrt-commerce.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ - function aksrt_get_commerce_nav_items(): array { - $items = []; - - $cart = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; - if ($cart !== '') { - $items[] = [ - 'key' => 'cart', - 'label' => __('购物车', 'aksrt'), - 'url' => $cart, - ]; - } - - $orders = function_exists('aksrt_get_orders_page_url') ? aksrt_get_orders_page_url() : ''; - if ($orders !== '') { - $items[] = [ - 'key' => 'orders', - 'label' => __('我的订单', 'aksrt'), - 'url' => $orders, - ]; - } - - $revenue = function_exists('aksrt_get_revenue_page_url') ? aksrt_get_revenue_page_url() : ''; - if ($revenue !== '') { - $items[] = [ - 'key' => 'revenue', - 'label' => __('创作收益', 'aksrt'), - 'url' => $revenue, - ]; - } - - $center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : ''; - if ($center !== '') { - $items[] = [ - 'key' => 'center', - 'label' => __('个人中心', 'aksrt'), - 'url' => $center, - ]; - } - - return $items; - } -} - -if (!function_exists('aksrt_render_commerce_page_header')) { - /** - * Shared commerce page header: title / intro / secondary nav. - * - * @param array{title?:string,subtitle?:string,active?:string} $args - */ - function aksrt_render_commerce_page_header(array $args = []): void { - $title = (string) ($args['title'] ?? ''); - $subtitle = (string) ($args['subtitle'] ?? ''); - $active = sanitize_key((string) ($args['active'] ?? '')); - $nav = aksrt_get_commerce_nav_items(); - ?> -
- -

- - -

- - - - -
- __('下载消费', 'aksrt'), 'admin' => __('管理员调整', 'aksrt'), 'refund' => __('退款', 'aksrt'), - 'forum_topic' => __('论坛发帖', 'aksrt'), - 'forum_reply' => __('论坛回复', 'aksrt'), 'free_sign_up' => __('注册奖励', 'aksrt'), 'free_sign_in' => __('每日签到', 'aksrt'), 'free_login' => __('每日登录', 'aksrt'), @@ -58,13 +56,6 @@ if (!function_exists('aksrt_credits_label')) { 'free_post_favorite'=> __('文章被收藏', 'aksrt'), 'free_comment_new'=> __('发表评论', 'aksrt'), 'free_comment_like'=> __('评论被赞', 'aksrt'), - 'free_bbs_posts_new'=> __('论坛发帖', 'aksrt'), - 'free_bbs_reply'=> __('论坛回复', 'aksrt'), - 'free_bbs_essence'=> __('帖子加精', 'aksrt'), - 'free_bbs_adopt'=> __('回复被采纳', 'aksrt'), - 'free_bbs_posts_hot'=> __('热帖奖励', 'aksrt'), - 'free_bbs_comment_hot'=> __('神评奖励', 'aksrt'), - 'free_bbs_score_extra'=> __('帖子加分', 'aksrt'), ]; if (isset($map[$reason])) { return $map[$reason]; diff --git a/inc/helpers/aksrt-forum-widgets.php b/inc/helpers/aksrt-forum-widgets.php deleted file mode 100644 index 49ff9e22..00000000 --- a/inc/helpers/aksrt-forum-widgets.php +++ /dev/null @@ -1,363 +0,0 @@ - (string) get_post_meta($post_id, '_aksrt_topping', true) === '1' - || (string) get_post_meta($post_id, '_aksrt_is_top', true) === '1', - 'essence' => (string) get_post_meta($post_id, '_aksrt_essence', true) === '1', - 'views' => (int) get_post_meta($post_id, '_aksrt_views', true), - 'replies' => (int) get_comments_number($post_id), - 'plate_id' => (int) get_post_meta($post_id, '_aksrt_plate_id', true), - ]; - } -endif; - -if (!function_exists('aksrt_forum_topic_badges_html')) : - /** - * Topping / essence badges for forum topic rows. - */ - function aksrt_forum_topic_badges_html(int $post_id, bool $show_topping = true): string { - $flags = aksrt_forum_topic_meta_flags($post_id); - $html = ''; - if ($show_topping && !empty($flags['topping'])) { - $html .= '' . esc_html__('置顶', 'aksrt') . ''; - } - if (!empty($flags['essence'])) { - $html .= '' . esc_html__('精华', 'aksrt') . ''; - } - return $html; - } -endif; - -if (!function_exists('aksrt_render_forum_topic_mini')) : - /** - * Mini forum topic row (zibll zib_bbs_get_posts_mini_list parity). - * - * @param int|null $post_id Null = current post in loop. - * @param string $class Extra classes on outer element. - * @param bool $show_topping Whether to show topping badge. - */ - function aksrt_render_forum_topic_mini(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string { - $post_id = $post_id ?: (int) get_the_ID(); - if ($post_id <= 0) { - return ''; - } - $post = get_post($post_id); - if (!$post || $post->post_status !== 'publish') { - return ''; - } - - $author_id = (int) $post->post_author; - $flags = aksrt_forum_topic_meta_flags($post_id); - $permalink = get_permalink($post_id); - $title = get_the_title($post_id); - - $avatar_url = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($author_id, ['size' => 48]) - : get_avatar_url($author_id, ['size' => 48]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($author_id) - : get_author_posts_url($author_id); - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $author_id, - 'class' => 'flex ac flex1', - 'follow' => false, - 'medal' => false, - 'level' => false, - ]) - : '' . esc_html(get_the_author_meta('display_name', $author_id)) . ''; - - // Prefer last reply time when available. - $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true); - if ($time_raw === '' || !strtotime($time_raw)) { - $time_raw = (string) $post->post_date; - } - $time_txt = function_exists('aksrt_time_ago') - ? aksrt_time_ago($time_raw) - : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt'); - - $views_txt = function_exists('aksrt_format_number') - ? aksrt_format_number($flags['views']) - : (string) $flags['views']; - - $cls = trim('forum-posts mini aksrt-ftopic-mini relative ' . $class); - $html = ''; - $html .= aksrt_forum_topic_badges_html($post_id, $show_topping); - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= '' . $name . ''; - $html .= '' . esc_html($time_txt) . ''; - $html .= ' ' . esc_html((string) $views_txt) . ''; - if ($flags['replies'] > 0) { - $html .= ' ' . esc_html((string) $flags['replies']) . ''; - } - $html .= '
'; - - return $html; - } -endif; - -if (!function_exists('aksrt_render_forum_topic_detail')) : - /** - * Detail forum topic row (richer entry meta). - * - * @param int|null $post_id - * @param string $class - * @param bool $show_topping - */ - function aksrt_render_forum_topic_detail(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string { - $post_id = $post_id ?: (int) get_the_ID(); - if ($post_id <= 0) { - return ''; - } - $post = get_post($post_id); - if (!$post || $post->post_status !== 'publish') { - return ''; - } - - $author_id = (int) $post->post_author; - $flags = aksrt_forum_topic_meta_flags($post_id); - $permalink = get_permalink($post_id); - $title = get_the_title($post_id); - $excerpt = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 36, '…'); - - $avatar_url = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($author_id, ['size' => 64]) - : get_avatar_url($author_id, ['size' => 64]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($author_id) - : get_author_posts_url($author_id); - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $author_id, - 'class' => 'flex ac', - 'follow' => false, - 'medal' => true, - 'level' => true, - ]) - : '' . esc_html(get_the_author_meta('display_name', $author_id)) . ''; - - $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true); - if ($time_raw === '' || !strtotime($time_raw)) { - $time_raw = (string) $post->post_date; - } - $time_txt = function_exists('aksrt_time_ago') - ? aksrt_time_ago($time_raw) - : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt'); - - $views_txt = function_exists('aksrt_format_number') - ? aksrt_format_number($flags['views']) - : (string) $flags['views']; - - // Optional plate name chip. - $plate_chip = ''; - if ($flags['plate_id'] > 0) { - $plate = get_post($flags['plate_id']); - if ($plate instanceof WP_Post) { - $plate_chip = '' . esc_html(get_the_title($plate)) . ''; - } - } - - $cls = trim('forum-posts detail aksrt-ftopic-detail relative ' . $class); - $html = ''; - $html .= aksrt_forum_topic_badges_html($post_id, $show_topping); - $html .= '
'; - $html .= ''; - $html .= '
' . $name . '
' . esc_html($time_txt) . '
'; - $html .= '
'; - $html .= '

' . esc_html($title) . '

'; - if ($excerpt !== '') { - $html .= '
' . esc_html($excerpt) . '
'; - } - $html .= '
'; - $html .= $plate_chip; - $html .= ' ' . esc_html((string) $views_txt) . ''; - $html .= ' ' . esc_html((string) $flags['replies']) . ''; - $html .= '
'; - - return $html; - } -endif; - -if (!function_exists('aksrt_render_plate_thumb_html')) : - /** - * Shared plate thumb with lazy pattern. - */ - function aksrt_render_plate_thumb_html(int $plate_id, string $size = 'thumbnail'): string { - $thumb = get_the_post_thumbnail_url($plate_id, $size); - $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - if ($thumb) { - return ''; - } - if ($icon !== '') { - return ''; - } - return ''; - } -endif; - -if (!function_exists('aksrt_render_plate_row')) : - /** - * Plate list row (zibll plate-item flex). - * - * @param WP_Post|int $plate - */ - function aksrt_render_plate_row($plate): string { - $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate); - if (!$post instanceof WP_Post) { - return ''; - } - $id = (int) $post->ID; - $url = get_permalink($id); - $count = (int) get_post_meta($id, '_aksrt_posts_count', true); - $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); - $desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 18); - - $html = ''; - $html .= aksrt_render_plate_thumb_html($id, 'thumbnail'); - $html .= '
'; - $html .= '

' . esc_html(get_the_title($post)) . '

'; - if ($desc !== '') { - $html .= '
' . esc_html($desc) . '
'; - } - $html .= '
'; - $html .= '' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . ''; - if ($follow > 0) { - $html .= '' . esc_html(sprintf(/* translators: %d follow count */ __('%d 关注', 'aksrt'), $follow)) . ''; - } - $html .= '
'; - return $html; - } -endif; - -if (!function_exists('aksrt_render_plate_card')) : - /** - * Plate card slide item. - * - * @param WP_Post|int $plate - */ - function aksrt_render_plate_card($plate): string { - $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate); - if (!$post instanceof WP_Post) { - return ''; - } - $id = (int) $post->ID; - $url = get_permalink($id); - $thumb = get_the_post_thumbnail_url($id, 'medium'); - $count = (int) get_post_meta($id, '_aksrt_posts_count', true); - $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - - $html = ''; - $html .= '
'; - if ($thumb) { - $html .= ''; - } else { - $html .= ''; - } - $html .= '
'; - $html .= '
' . esc_html(get_the_title($post)) . '
'; - $html .= '
'; - $html .= '' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . ''; - $html .= '' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . ''; - $html .= '
'; - return $html; - } -endif; - -if (!function_exists('aksrt_render_moderator_row')) : - /** - * Moderator identity row with optional follow. - * - * @param int $user_id - * @param string $role author|super|moderator - * @param bool $show_avatar - * @param string $style list|grid - */ - function aksrt_render_moderator_row(int $user_id, string $role = 'moderator', bool $show_avatar = true, string $style = 'list'): string { - if ($user_id <= 0) { - return ''; - } - $user = get_userdata($user_id); - if (!$user) { - return ''; - } - $avatar = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($user_id, ['size' => 64]) - : get_avatar_url($user_id, ['size' => 64]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($user_id) - : get_author_posts_url($user_id); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - - if ($role === 'author' || $role === 'super') { - $role_label = __('超级版主', 'aksrt'); - } else { - $role_label = __('版主', 'aksrt'); - } - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $user_id, - 'class' => 'flex ac flex1', - 'follow' => false, - 'medal' => true, - 'level' => true, - ]) - : '' . esc_html($user->display_name) . ''; - - // Follow control (not for self). - $follow_html = ''; - $cuid = get_current_user_id(); - if ($cuid > 0 && $cuid !== $user_id && class_exists('\AKSRT\User\Follow')) { - $following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $user_id); - $label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt'); - $follow_html = ''; - } - - if ($style === 'grid') { - $html = '
'; - if ($show_avatar) { - $html .= ''; - } - $html .= '
' . $name . '
'; - $html .= '
' . esc_html($role_label) . '
'; - $html .= $follow_html; - $html .= '
'; - return $html; - } - - $html = '
'; - if ($show_avatar) { - $html .= ''; - } - $html .= '
'; - $html .= '
' . $name . '
'; - $html .= '
' . esc_html($role_label) . '
'; - $html .= '
'; - $html .= $follow_html; - $html .= '
'; - return $html; - } -endif; diff --git a/inc/helpers/aksrt-forum.php b/inc/helpers/aksrt-forum.php index e33e7883..1bb49709 100644 --- a/inc/helpers/aksrt-forum.php +++ b/inc/helpers/aksrt-forum.php @@ -1,8 +1,9 @@ $args Optional query args (board, plate, …). - */ - function aksrt_get_forum_compose_url(array $args = []): string { - $url = aksrt_get_page_url_by_template('templates/forum/forum-compose.php'); - if ($url === '') { - $url = home_url('/'); - } - - // Normalize plate → board for compose form contract. - if (isset($args['plate']) && !isset($args['board'])) { - $args['board'] = $args['plate']; - unset($args['plate']); - } - - $clean = []; - foreach ($args as $key => $value) { - $key = sanitize_key((string) $key); - if ($key === '') { - continue; - } - if (is_bool($value)) { - $clean[$key] = $value ? '1' : '0'; - } else { - $clean[$key] = (string) $value; - } - } - - return !empty($clean) ? add_query_arg($clean, $url) : $url; - } -endif; diff --git a/inc/helpers/aksrt-header-menu-buttons.php b/inc/helpers/aksrt-header-menu-buttons.php index 8a0236c0..a6cad02c 100644 --- a/inc/helpers/aksrt-header-menu-buttons.php +++ b/inc/helpers/aksrt-header-menu-buttons.php @@ -21,17 +21,6 @@ if (!function_exists('aksrt_get_header_newposts_config')) : $types = is_array($cfg['types'] ?? null) ? $cfg['types'] : []; if (!$types) { $types = ['post']; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - if (!empty($forum['enabled'])) { - $types[] = 'topic'; - $types[] = 'plate'; - } - if (!empty($shop['enabled'])) { - $types[] = 'product'; - } - } } $cfg['types'] = $types; if (empty($cfg['class'])) { @@ -65,53 +54,6 @@ if (!function_exists('aksrt_get_new_add_actions')) : 'label' => __('写文章', 'aksrt'), 'icon' => 'fas fa-file-alt', ]; - } elseif ($type === 'topic') { - $url = function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : ''; - if ($url === '') { - continue; - } - $out[] = [ - 'type' => 'topic', - 'url' => $url, - 'label' => __('发帖', 'aksrt'), - 'icon' => 'fas fa-comments', - ]; - } elseif ($type === 'product') { - $url = ''; - if (function_exists('aksrt_get_product_edit_url')) { - $url = (string) aksrt_get_product_edit_url(); - } - if ($url === '') { - $url = admin_url('post-new.php?post_type=aksrt_product'); - } - $out[] = [ - 'type' => 'product', - 'url' => $url, - 'label' => __('发布商品', 'aksrt'), - 'icon' => 'fas fa-shopping-bag', - ]; - } elseif ($type === 'plate') { - // zibll bbs_plate → AKSRT forum plate CPT (admin create for entitled users). - $can = current_user_can('manage_options'); - if (!$can && class_exists('\AKSRT\Forum\Forum')) { - try { - $forum = \AKSRT\Forum\Forum::get_instance(); - if (method_exists($forum, 'forum_user_can')) { - $can = (bool) $forum->forum_user_can(get_current_user_id(), 'plate_create'); - } - } catch (\Throwable $e) { - $can = false; - } - } - if (!$can || !post_type_exists('aksrt_forum_plate')) { - continue; - } - $out[] = [ - 'type' => 'plate', - 'url' => admin_url('post-new.php?post_type=aksrt_forum_plate'), - 'label' => __('创建版块', 'aksrt'), - 'icon' => 'fas fa-th-large', - ]; } } return $out; diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 5d0531fa..adcb9ec0 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -356,26 +356,7 @@ if (!function_exists('aksrt_content_order_cookie_token_key')) { } /** - * Whether guest cash purchase is allowed for forum topic / plate follow. - * Reuses global pay_no_logged_in (same switch as content). - * - * @param WP_Post|int|null $post Topic or plate post (optional filter context). - */ -if (!function_exists('aksrt_allow_guest_forum_pay')) { - function aksrt_allow_guest_forum_pay($post = null): bool { - $order = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_order_settings() : []; - $paid = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_paid_content_settings() : []; - $allow = !empty($order['no_logged_in']) || !empty($paid['no_logged_in']); - /** - * @param bool $allow - * @param WP_Post|int|null $post - */ - return (bool) apply_filters('aksrt_allow_guest_forum_pay', $allow, $post); - } -} - -/** - * Set guest paid cookies after cash order paid (content / topic / plate_follow). + * Set guest paid cookies after cash order paid (content). * * @param array $order */ @@ -495,18 +476,6 @@ if (!function_exists('aksrt_guest_has_paid_access')) { } } -/** - * Guest paid plate follow access (cookie). - */ -if (!function_exists('aksrt_guest_has_plate_follow_access')) { - function aksrt_guest_has_plate_follow_access(int $plate_id): bool { - $type = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW - : 'plate_follow'; - return null !== aksrt_get_guest_paid_order($plate_id, $type); - } -} - /** * Resolve the amount a given user should pay for a post (Zibll VIP tier min). * diff --git a/inc/helpers/aksrt-runtime-win.php b/inc/helpers/aksrt-runtime-win.php index 18c5595d..ab0e7dcf 100644 --- a/inc/helpers/aksrt-runtime-win.php +++ b/inc/helpers/aksrt-runtime-win.php @@ -7,51 +7,6 @@ defined('ABSPATH') || exit; -if (!function_exists('aksrt_get_float_shop_cart_html')) : - /** - * Floating shop cart button with badge. - */ - function aksrt_get_float_shop_cart_html(array $item = []): string { - if (!is_user_logged_in()) { - return ''; - } - $shop_on = true; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_on = !empty($shop['enabled']); - } - if (!$shop_on) { - return ''; - } - // Skip on cart page. - if (function_exists('is_page_template') && is_page_template('templates/pages/page-cart.php')) { - return ''; - } - $url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; - if ($url === '') { - return ''; - } - $count = 0; - if (class_exists('\AKSRT\Shop\Shop')) { - $count = (int) \AKSRT\Shop\Shop::get_instance()->get_cart_count(get_current_user_id()); - } - $icon = !empty($item['icon']) ? (string) $item['icon'] : 'fas fa-shopping-cart'; - $style = []; - if (!empty($item['color'])) { - $style[] = '--this-color:' . esc_attr((string) $item['color']); - } - if (!empty($item['bg'])) { - $style[] = '--this-bg:' . esc_attr((string) $item['bg']); - } - $style_attr = $style ? ' style="' . implode(';', $style) . '"' : ''; - $badge = $count > 0 ? (string) min(99, $count) : ''; - return '' - . '' - . '' . esc_html($badge) . '' - . ''; - } -endif; - if (!function_exists('aksrt_get_float_pay_vip_html')) : /** * Floating VIP open button. diff --git a/inc/helpers/aksrt-transactions.php b/inc/helpers/aksrt-transactions.php index d190592c..8217a609 100644 --- a/inc/helpers/aksrt-transactions.php +++ b/inc/helpers/aksrt-transactions.php @@ -17,41 +17,6 @@ function aksrt_get_order_status_labels(): array { return []; } -function aksrt_get_product_page_view(int $product_id): array { - if ($product_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return []; - } - - return \AKSRT\Shop\Shop::get_instance()->get_product_detail_view($product_id); -} - -function aksrt_get_product_edit_page_view(int $user_id, int $product_id = 0): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return [ - 'allowed' => false, - 'is_edit' => false, - 'product' => [], - ]; - } - - return \AKSRT\Shop\Shop::get_instance()->get_product_edit_view($user_id, $product_id); -} - -function aksrt_get_cart_page_view(int $user_id): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return [ - 'payment_methods' => [], - 'shipping_methods' => [], - 'address_book' => [], - 'coupon_enabled' => false, - 'currency_symbol' => '¥', - 'pending_orders' => [], - ]; - } - - return \AKSRT\Shop\Shop::get_instance()->get_cart_page_view($user_id); -} - function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array { $empty = [ 'items' => [], @@ -79,10 +44,6 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args); } - if (class_exists('\AKSRT\Shop\Shop')) { - return \AKSRT\Shop\Shop::get_instance()->get_order_center_view($user_id, $args); - } - return $empty; } @@ -135,4 +96,4 @@ function aksrt_get_revenue_page_view(int $user_id): array { 'total_withdrawn' => $total_withdrawn, 'records' => $records, ]; -} +} \ No newline at end of file diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php index a071718d..a0f67678 100644 --- a/inc/helpers/aksrt-user-center.php +++ b/inc/helpers/aksrt-user-center.php @@ -485,18 +485,8 @@ if (!function_exists('aksrt_get_user_center_render_context')) { $wallet_balance = (float) ($dashboard['wallet_balance'] ?? 0.0); $runtime_config = aksrt_user_center_runtime_config($user_id, $tabs['default'], $active_tab, $tabs['urls']); $runtime_config['avatarUpload'] = aksrt_user_center_avatar_upload_config($allow_avatar_upload); - $shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $forum_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_forum_settings() : []; $revenue_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_revenue_settings() : []; - $shop_enabled = !empty($shop_settings['enabled']); - $forum_enabled = !empty($forum_settings['enabled']); $new_post_url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : ''; - $new_product_url = $shop_enabled && function_exists('aksrt_get_page_url_by_template') - ? aksrt_get_page_url_by_template('templates/pages/page-product-edit.php') - : ''; - $forum_compose_url = $forum_enabled && function_exists('aksrt_get_page_url_by_template') - ? aksrt_get_page_url_by_template('templates/forum/forum-compose.php') - : ''; $revenue_url = (!empty($revenue_settings['enabled']) || !empty($affiliate_settings['enabled'])) && function_exists('aksrt_get_revenue_page_url') ? aksrt_get_revenue_page_url() : ''; @@ -523,8 +513,6 @@ if (!function_exists('aksrt_get_user_center_render_context')) { 'notification_unread_count' => function_exists('aksrt_get_user_unread_notification_count') ? aksrt_get_user_unread_notification_count($user_id) : 0, 'messages_url' => $messages_enabled && function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '', 'new_post_url' => $new_post_url, - 'new_product_url' => $new_product_url, - 'forum_compose_url' => $forum_compose_url, 'revenue_url' => $revenue_url, 'per_page' => 10, 'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1), diff --git a/inc/helpers/aksrt-user-home.php b/inc/helpers/aksrt-user-home.php index 0330a41a..49c2fb25 100644 --- a/inc/helpers/aksrt-user-home.php +++ b/inc/helpers/aksrt-user-home.php @@ -79,52 +79,16 @@ if (!function_exists('aksrt_get_user_home_page_context')) { $display_tabs = $is_mobile ? $enabled_tabs_mobile : $enabled_tabs_pc; $default_tab = !empty($display_tabs) ? $display_tabs[0] : $enabled_tabs[0]; - $forum_instance = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null; - $forum_stats = $forum_instance ? $forum_instance->get_user_forum_stats($user_id) : []; - $forum_topic_count = (int) ($forum_stats['topic_count'] ?? 0); - $product_count = 0; - if (class_exists('\AKSRT\Shop\Shop')) { - $product_count = (int) \AKSRT\Shop\Shop::get_instance()->count_author_products($user_id); - } - $follow_count = 0; if (class_exists('\AKSRT\User\Follow')) { $follow_count = (int) \AKSRT\User\Follow::get_instance()->get_follower_count($user_id); } - $plate_count = 0; - if ($forum_instance) { - $seen_plates = []; - if (method_exists($forum_instance, 'get_user_followed_plates')) { - foreach ((array) $forum_instance->get_user_followed_plates($user_id) as $plate) { - $pid = (int) ($plate['id'] ?? 0); - if ($pid > 0) { - $seen_plates[$pid] = true; - } - } - } - if (method_exists($forum_instance, 'get_user_managed_plates')) { - $managed = (array) $forum_instance->get_user_managed_plates($user_id); - foreach (['owned', 'moderating'] as $bucket) { - foreach ((array) ($managed[$bucket] ?? []) as $plate) { - $pid = (int) ($plate['id'] ?? 0); - if ($pid > 0) { - $seen_plates[$pid] = true; - } - } - } - } - $plate_count = count($seen_plates); - } - $tab_counts = [ 'posts' => $post_count, 'comments' => $comment_count, 'favorites' => $favorite_count, 'credits' => $credits_enabled && $is_own_profile ? (int) round((float) $credit_balance) : 0, - 'product' => $product_count, - 'plate' => $plate_count, - 'forum' => $forum_topic_count, 'follow' => $follow_count, ]; @@ -133,9 +97,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) { 'comments' => __('评论', 'aksrt'), 'favorites' => __('收藏', 'aksrt'), 'credits' => sprintf(__('我的%s', 'aksrt'), $currency_name), - 'product' => __('商品', 'aksrt'), - 'plate' => __('圈子', 'aksrt'), - 'forum' => __('论坛', 'aksrt'), 'follow' => __('粉丝', 'aksrt'), // Zibll: 粉丝 ]; foreach ($visible_tabs as $tab_key => $tab_config) { @@ -174,9 +135,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) { 'active_tab' => $active_tab, 'tab_counts' => $tab_counts, 'tab_labels' => $tab_labels, - 'forum_instance' => $forum_instance, - 'forum_stats' => $forum_stats, - 'forum_topic_count' => $forum_topic_count, 'home_url' => $home_url, 'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1), 'comments_page' => max(1, isset($_GET['comments_page']) ? (int) $_GET['comments_page'] : 1), diff --git a/inc/helpers/header/aksrt-header-navbar.php b/inc/helpers/header/aksrt-header-navbar.php index ccc50245..2ef71fce 100644 --- a/inc/helpers/header/aksrt-header-navbar.php +++ b/inc/helpers/header/aksrt-header-navbar.php @@ -208,19 +208,7 @@ if (!function_exists('aksrt_header_user_box_html')) : if ($pub_text === '') { $pub_text = __('发布内容', 'aksrt'); } - $pub_type = sanitize_key((string) ($all['header_user_newposts_btn'] ?? 'post')); $pub_url = (string) $ctx['publish_url']; - if ($pub_type === 'topic' && function_exists('aksrt_get_forum_compose_url')) { - $u = aksrt_get_forum_compose_url(); - if ($u) { $pub_url = $u; } - } elseif ($pub_type === 'product') { - if (function_exists('aksrt_get_product_edit_url')) { - $u = (string) aksrt_get_product_edit_url(); - if ($u !== '') { $pub_url = $u; } - } else { - $pub_url = admin_url('post-new.php?post_type=aksrt_product'); - } - } $href .= ''; $href .= '
'; $href .= '
' . esc_html($pub_text) . '
'; @@ -240,13 +228,6 @@ if (!function_exists('aksrt_header_user_box_html')) : $href .= ''; $href .= '
'; $href .= '
' . esc_html__('模块布局', 'aksrt') . '
'; - // zibll: 商城中心. - $shop_url = post_type_exists('aksrt_product') - ? admin_url('edit.php?post_type=aksrt_product') - : admin_url('admin.php?page=aksrt_pay'); - $href .= ''; - $href .= '
'; - $href .= '
' . esc_html__('商城中心', 'aksrt') . '
'; $href .= ''; $href .= '
'; $href .= '
' . esc_html__('后台管理', 'aksrt') . '
'; diff --git a/inc/helpers/homepage/header-components.php b/inc/helpers/homepage/header-components.php index e9fd63d3..e9db142c 100644 --- a/inc/helpers/homepage/header-components.php +++ b/inc/helpers/homepage/header-components.php @@ -22,9 +22,6 @@ if (!function_exists('aksrt_header_components_page_type_keys')) { 'author', 'single', 'search', - 'forum_home', - 'forum_plate', - 'forum_post', 'page', ]; } @@ -46,7 +43,6 @@ if (!function_exists('aksrt_header_slide_excluded_page_templates')) { 'templates/pages/page-verify-email.php', 'templates/pages/page-download.php', 'templates/pages/page-download-file.php', - 'templates/pages/page-forums.php', ]; } } @@ -77,18 +73,6 @@ if (!function_exists('aksrt_header_slide_match_page_type')) { return is_tax('topics'); } - if ($type === 'forum_home') { - return is_page_template('templates/pages/page-forums.php'); - } - - if ($type === 'forum_plate') { - return is_singular('aksrt_forum_plate') || $post_type === 'aksrt_forum_plate'; - } - - if ($type === 'forum_post') { - return is_singular('aksrt_forum_topic') || $post_type === 'aksrt_forum_topic'; - } - // Generic WP_Query is_* flags (home/category/tag/author/single/search/page). $query_flag = 'is_' . $type; if (empty($wp_query->{$query_flag})) { diff --git a/inc/helpers/user-center-renderers/credits-points-tasks.php b/inc/helpers/user-center-renderers/credits-points-tasks.php index 2a438f49..00af0571 100644 --- a/inc/helpers/user-center-renderers/credits-points-tasks.php +++ b/inc/helpers/user-center-renderers/credits-points-tasks.php @@ -23,22 +23,6 @@ $aksrt_points_task_action = static function (string $key): array { $submit = (string) aksrt_get_page_url_by_template('templates/pages/page-submit.php'); } - $forum_on = false; - $forum_home = ''; - $forum_compose = ''; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - $forum_on = !empty($f['enabled']); - } - if ($forum_on) { - if (function_exists('aksrt_get_forum_home_url')) { - $forum_home = (string) aksrt_get_forum_home_url(); - } - if (function_exists('aksrt_get_forum_compose_url')) { - $forum_compose = (string) aksrt_get_forum_compose_url(); - } - } - switch ($key) { case 'sign_in': return [ @@ -93,37 +77,10 @@ $aksrt_points_task_action = static function (string $key): array { 'label' => __('去主页', 'aksrt'), 'href' => $center !== '' ? $center : $home, ]; - case 'bbs_posts_new': - case 'bbs_reply': - if (!$forum_on) { + default: return ['type' => 'none', 'label' => '']; - } - $href = $forum_compose !== '' ? $forum_compose : $forum_home; - if ($href === '') { - return ['type' => 'none', 'label' => '']; - } - return [ - 'type' => 'link', - 'label' => __('去完成', 'aksrt'), - 'href' => $href, - ]; - case 'bbs_essence': - case 'bbs_adopt': - case 'bbs_posts_hot': - case 'bbs_comment_hot': - case 'bbs_score_extra': - if (!$forum_on || $forum_home === '') { - return ['type' => 'none', 'label' => '']; - } - return [ - 'type' => 'link', - 'label' => __('去看看', 'aksrt'), - 'href' => $forum_home, - ]; - default: - return ['type' => 'none', 'label' => '']; - } -}; + } + }; $user_id = (int) ($user_id ?? get_current_user_id()); $today = \AKSRT\Credits\PointsFree::today_total($user_id); diff --git a/inc/helpers/user-center-renderers/profile-orders.php b/inc/helpers/user-center-renderers/profile-orders.php index 2e009bee..bf1d6d67 100644 --- a/inc/helpers/user-center-renderers/profile-orders.php +++ b/inc/helpers/user-center-renderers/profile-orders.php @@ -25,7 +25,7 @@ $section = sanitize_key((string) ($_GET['section'] ?? '')); if ($tab === '' && isset($_GET['tab']) && sanitize_key((string) $_GET['tab']) !== 'orders') { // When rendered as standalone legacy include, outer tab may be status. $maybe = sanitize_key((string) $_GET['tab']); - if (in_array($maybe, ['pending', 'to-ship', 'shipped', 'received', 'review', 'after'], true)) { + if (in_array($maybe, ['pending', 'after'], true)) { $tab = $maybe; } } @@ -37,9 +37,6 @@ if (isset($tab_aliases[$tab])) { $tab_status_map = [ 'pending' => \AKSRT\Payment\Order::STATUS_PENDING, - 'to-ship' => \AKSRT\Payment\Order::STATUS_PAID, - 'shipped' => \AKSRT\Payment\Order::STATUS_DELIVERED, - 'review' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'after' => \AKSRT\Payment\Order::STATUS_REFUNDING, ]; if ($tab !== '' && isset($tab_status_map[$tab])) { @@ -58,15 +55,10 @@ if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) { $type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()); } -$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; - $tabs = [ '' => __('全部', 'aksrt'), 'pending' => __('待支付', 'aksrt'), - 'to-ship' => __('待发货', 'aksrt'), - 'shipped' => __('待收货', 'aksrt'), - 'review' => __('待评价', 'aksrt'), - 'after' => __('售后', 'aksrt'), + 'after' => __('退款中', 'aksrt'), ]; $base_query = ['tab' => 'orders']; @@ -115,16 +107,8 @@ $pending_count = (int) ($stats['pending'] ?? 0); -
+
- - - - - - - -
@@ -145,9 +129,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);
- -
@@ -304,23 +286,11 @@ $pending_count = (int) ($stats['pending'] ?? 0); - - - - - - - - - - - -
@@ -348,7 +318,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);

-

+

@@ -367,114 +337,6 @@ $pending_count = (int) ($stats['pending'] ?? 0); - - - - - - - get_user_tickets($user_id, 50); - ?> -
-

- -
- -
-
- # - - -
-
- - · -
-
- -
    - -
  1. - - -
  2. - -
- -
- - - - - - -
-
- -
- -

- -
- -

@@ -483,27 +345,4 @@ $pending_count = (int) ($stats['pending'] ?? 0);
- - get_user_favorites($user_id); - ?> -
-

- - - -

- -
- diff --git a/inc/helpers/user-center-renderers/sidebar-nav.php b/inc/helpers/user-center-renderers/sidebar-nav.php index e9520b48..9671b9e4 100644 --- a/inc/helpers/user-center-renderers/sidebar-nav.php +++ b/inc/helpers/user-center-renderers/sidebar-nav.php @@ -7,19 +7,12 @@ defined('ABSPATH') || exit; -$cart_url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; $level_url = isset($dashboard_tabs['level']) ? aksrt_get_user_center_tab_url('level') : ''; $auth_url = isset($dashboard_tabs['auth']) ? aksrt_get_user_center_tab_url('auth') : ''; $services = array_values(array_filter([ - [ - 'label' => __('购物车', 'aksrt'), - 'icon' => 'shopping-cart', - 'tone' => 'cart', - 'url' => $cart_url, - ], [ 'label' => __('我的等级', 'aksrt'), 'icon' => 'chart-line', diff --git a/inc/helpers/user-center-renderers/sidebar-orders.php b/inc/helpers/user-center-renderers/sidebar-orders.php index b6f898cc..e0710a57 100644 --- a/inc/helpers/user-center-renderers/sidebar-orders.php +++ b/inc/helpers/user-center-renderers/sidebar-orders.php @@ -19,12 +19,6 @@ $order_stats = class_exists('\AKSRT\Payment\Order') $order_repo = class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance() : null; -$shop_type = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP - : 'shop_product'; -$shop_paid = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PAID, 'product_type' => $shop_type]) : 0; -$shop_delivered = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'product_type' => $shop_type]) : 0; -$shop_review = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'product_type' => $shop_type]) : 0; $order_items = [ [ 'label' => __('待支付', 'aksrt'), @@ -32,24 +26,6 @@ $order_items = [ 'tab' => 'pending', 'count' => (int) ($order_stats['pending'] ?? 0), ], - [ - 'label' => __('待发货', 'aksrt'), - 'icon' => 'gift', - 'tab' => 'to-ship', - 'count' => $shop_paid, - ], - [ - 'label' => __('待收货', 'aksrt'), - 'icon' => 'truck', - 'tab' => 'shipped', - 'count' => $shop_delivered, - ], - [ - 'label' => __('待评价', 'aksrt'), - 'icon' => 'comment-dots', - 'tab' => 'review', - 'count' => $shop_review, - ], [ 'label' => __('售后', 'aksrt'), 'icon' => 'undo-alt', diff --git a/inc/modules/user-parts/routing.php b/inc/modules/user-parts/routing.php index 27bbdd3c..14e6e815 100644 --- a/inc/modules/user-parts/routing.php +++ b/inc/modules/user-parts/routing.php @@ -16,7 +16,6 @@ add_filter('query_vars', function ($vars) { $vars[] = 'aksrt_user_center'; $vars[] = 'user_nicename'; $vars[] = 'aksrt_orders'; - $vars[] = 'aksrt_cart'; $vars[] = 'aksrt_revenue'; return $vars; }); @@ -40,13 +39,11 @@ add_action('init', function () { add_rewrite_tag('%aksrt_user_center%', '([^&]+)'); add_rewrite_tag('%user_nicename%', '([^/]+)'); add_rewrite_tag('%aksrt_orders%', '([0-1])'); - add_rewrite_tag('%aksrt_cart%', '([0-1])'); add_rewrite_tag('%aksrt_revenue%', '([0-1])'); add_rewrite_rule('^profile/?$', 'index.php?aksrt_user_center=1', 'top'); add_rewrite_rule('^user/([^/]+)/?$', 'index.php?user_nicename=$matches[1]', 'top'); // Commerce center aliases (work even before WP page is assigned). add_rewrite_rule('^orders/?$', 'index.php?aksrt_orders=1', 'top'); - add_rewrite_rule('^cart/?$', 'index.php?aksrt_cart=1', 'top'); add_rewrite_rule('^revenue/?$', 'index.php?aksrt_revenue=1', 'top'); }); diff --git a/inc/modules/user-parts/template-registry.php b/inc/modules/user-parts/template-registry.php index 263f6178..0923d6f7 100644 --- a/inc/modules/user-parts/template-registry.php +++ b/inc/modules/user-parts/template-registry.php @@ -113,12 +113,6 @@ $_aksrt_special_templates = [ 'title' => '我的订单', 'fallback' => '/orders/', ], - 'cart' => [ - 'file' => 'templates/pages/page-cart.php', - 'slug' => 'cart', - 'title' => '购物车', - 'fallback' => '/cart/', - ], 'revenue' => [ 'file' => 'templates/pages/page-revenue.php', 'slug' => 'revenue', @@ -137,20 +131,6 @@ $_aksrt_special_templates = [ 'title' => '发布文章', 'fallback' => '/submit/', ], - 'product-edit' => [ - 'file' => 'templates/pages/page-product-edit.php', - 'slug' => 'product-edit', - 'title' => '发布商品', - 'fallback' => '/product-edit/', - 'auto_create' => false, - ], - 'forum-compose' => [ - 'file' => 'templates/forum/forum-compose.php', - 'slug' => 'forum-compose', - 'title' => '论坛发帖', - 'fallback' => '/forum-compose/', - 'auto_create' => false, - ], ]; /** @var array 缓存 key=模板文件路径 => value=WP_Post|null */ @@ -427,11 +407,6 @@ function aksrt_get_current_special_template_path(): string { return $orders_template ?: ''; } - if (get_query_var('aksrt_cart')) { - $cart_template = aksrt_locate_template_file('templates/pages/page-cart.php'); - return $cart_template ?: ''; - } - if (get_query_var('aksrt_revenue')) { $revenue_template = aksrt_locate_template_file('templates/pages/page-revenue.php'); return $revenue_template ?: ''; diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index bbb546e3..ac478d3e 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -84,17 +84,6 @@ function aksrt_get_orders_page_url(): string { } endif; -if (!function_exists('aksrt_get_cart_page_url')) : -/** - * 购物车页面 URL(模板 templates/pages/page-cart.php)。 - */ -function aksrt_get_cart_page_url(): string { - return function_exists('aksrt_get_template_url') - ? aksrt_get_template_url('cart') - : home_url('/cart/'); -} -endif; - if (!function_exists('aksrt_get_revenue_page_url')) : /** * 收益中心页面 URL(模板 templates/pages/page-revenue.php)。 diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index b0d5145f..82614bc4 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -92,42 +92,7 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetIconCoverCards'); register_widget('\AKSRT\Widgets\WidgetBookmarkLinks'); - - // Forum widgets - if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { - register_widget('\AKSRT\Widgets\WidgetForumBoardList'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumHotTopics'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumRecentTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumRecentTopics'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateLists'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateInfo')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateInfo'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateModerator'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { - register_widget('\AKSRT\Widgets\WidgetForumTopicLists'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTopicTerms')) { - register_widget('\AKSRT\Widgets\WidgetForumTopicTerms'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTabTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumTabTopics'); - } - - // Shop product widgets (§6.9) - register_widget('\AKSRT\Widgets\WidgetProductList'); - register_widget('\AKSRT\Widgets\WidgetProductOneline'); - register_widget('\AKSRT\Widgets\WidgetProductTab'); - - aksrt_maybe_repair_sidebar_blog_widgets(); + aksrt_maybe_repair_sidebar_blog_widgets(); } add_action('widgets_init', 'aksrt_register_widgets'); diff --git a/slim-forum.php b/slim-forum.php deleted file mode 100644 index 736407a7..00000000 --- a/slim-forum.php +++ /dev/null @@ -1,9 +0,0 @@ -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\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\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/src/AKSRT/Admin/ForumControllerPart1.php b/src/AKSRT/Admin/ForumControllerPart1.php deleted file mode 100644 index 92d57346..00000000 --- a/src/AKSRT/Admin/ForumControllerPart1.php +++ /dev/null @@ -1,390 +0,0 @@ -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\Forum::APPLICATION_PENDING], - ['label' => __('已通过', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_APPROVED], - ['label' => __('已拒绝', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_REJECTED], - ], - ], - ], - ]); - } -} diff --git a/src/AKSRT/Admin/ForumControllerPart2.php b/src/AKSRT/Admin/ForumControllerPart2.php deleted file mode 100644 index e99b9c1f..00000000 --- a/src/AKSRT/Admin/ForumControllerPart2.php +++ /dev/null @@ -1,188 +0,0 @@ -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\Forum { - return class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\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/src/AKSRT/Admin/GlobalControllerPart1.php b/src/AKSRT/Admin/GlobalControllerPart1.php index e1aa6795..53109d7d 100644 --- a/src/AKSRT/Admin/GlobalControllerPart1.php +++ b/src/AKSRT/Admin/GlobalControllerPart1.php @@ -185,7 +185,7 @@ trait GlobalControllerPart1 { } 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']; + $valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add']; $type = sanitize_text_field($item['type'] ?? 'custom'); if (!in_array($type, $valid_types, true)) { $type = 'custom'; diff --git a/src/AKSRT/Admin/Native/PayAfterSale.php b/src/AKSRT/Admin/Native/PayAfterSale.php deleted file mode 100644 index 5e65e751..00000000 --- a/src/AKSRT/Admin/Native/PayAfterSale.php +++ /dev/null @@ -1,357 +0,0 @@ -create([ - 'order_number' => $order_number, - 'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')), - 'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')), - 'amount' => (float) ($_POST['amount'] ?? 0), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - ]); - $ok = !is_wp_error($id) && $id > 0; - } - - $msg = $ok ? 'after_sale_created' : 'after_sale_create_failed'; - if ($redirect_page === 'after_sale') { - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - public function render_after_sale_page(): void { - $this->ensure_cap(); - $this->render_header(__('售后管理', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\AfterSale')) { - echo '

' . esc_html__('售后模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - if ($search === '' && isset($_GET['order_number'])) { - $search = sanitize_text_field((string) wp_unslash($_GET['order_number'])); - } - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = \AKSRT\Payment\AfterSale::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->after_sale_status_options(true); - $type_options = $this->after_sale_type_options(true); - - echo '

' . esc_html(sprintf( - /* translators: 1: total 2: pending */ - __('售后工单共 %1$d · 待处理 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['pending'] ?? 0) - )) . '

'; - - echo '

' . esc_html__('新建售后', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_after_sale'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建售后', 'aksrt'), 'primary', '', false); - echo '
'; - - echo '
'; - echo ''; - echo ' '; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $list_args = [ - 'status' => $status, - 'type' => $type, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - $list_args['order_number'] = $search; - } - $result = $svc->get_list($list_args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->after_sale_status_options(false); - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无售后工单', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - if ($st === 'pending') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('同意', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - echo ''; - echo '
'; - } - if ($st === 'processing') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('确认收货', 'aksrt'), 'small', '', false); - echo '
'; - } - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_after_sale_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - public function handle_update_after_sale(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_after_sale_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\AfterSale')) { - $ok = \AKSRT\Payment\AfterSale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note); - } - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed'])); - exit; - } - - public function handle_after_sale_action(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : ''; - check_admin_referer('aksrt_pay_after_sale_action_' . $id); - - $ok = false; - if ($id > 0 && class_exists('\AKSRT\Payment\AfterSale')) { - $svc = \AKSRT\Payment\AfterSale::get_instance(); - $operator_id = get_current_user_id(); - if ($as_action === 'approve') { - $ok = $svc->merchant_approve($id, $operator_id); - } elseif ($as_action === 'reject') { - $ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt')); - } elseif ($as_action === 'confirm_return') { - $ok = $svc->merchant_confirm_return($id, $operator_id); - } - } - - wp_safe_redirect($this->page_url('aksrt_after_sale', [ - 'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed', - ])); - exit; - } - - - private function after_sale_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); - } - if (class_exists('\AKSRT\Payment\AfterSale')) { - foreach (\AKSRT\Payment\AfterSale::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - return $options; - } - - /** - * @return array - */ - public function after_sale_type_options_public(bool $include_all = false): array { - return $this->after_sale_type_options($include_all); - } - - private function after_sale_type_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部类型', 'aksrt'); - } - // Keep operational create options broader than public get_type_options(). - $defaults = [ - 'refund' => __('仅退款', 'aksrt'), - 'refund_return' => __('退货退款', 'aksrt'), - 'exchange' => __('换货', 'aksrt'), - 'repair' => __('维修', 'aksrt'), - ]; - if (class_exists('\AKSRT\Payment\AfterSale')) { - foreach (\AKSRT\Payment\AfterSale::get_type_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $defaults[$value] = (string) ($item['label'] ?? $value); - } - } - return array_merge($options, $defaults); - } - - - private function ensure_cap(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); - } - } - - private function render_header(string $title): void { - echo '
'; - echo '

' . esc_html($title) . '

'; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
'; - } - - private function render_flash(): void { - if (empty($_GET['aksrt_pay_msg'])) { - return; - } - $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); - $map = [ - 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), - 'after_sale_failed' => __('售后状态更新失败。', 'aksrt'), - 'after_sale_created' => __('售后工单已创建。', 'aksrt'), - 'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'), - 'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'), - 'after_sale_action_failed' => __('售后操作失败。', 'aksrt'), - ]; - if (!isset($map[$code])) { - return; - } - $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; - echo '

' . esc_html($map[$code]) . '

'; - } - - private function page_url(string $slug, array $args = []): string { - return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); - } - - private function render_pagination(int $pages, int $page_num): void { - if ($pages <= 1) { - return; - } - echo '
'; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
'; - } -} diff --git a/src/AKSRT/Admin/Native/PayCoupons.php b/src/AKSRT/Admin/Native/PayCoupons.php index f9d69ad3..1fa0a4e5 100644 --- a/src/AKSRT/Admin/Native/PayCoupons.php +++ b/src/AKSRT/Admin/Native/PayCoupons.php @@ -372,7 +372,7 @@ class PayCoupons { 'ids' => $ids, ]; if ($type === 'category') { - $scope['taxonomies'] = ['aksrt_product_cat', 'category']; + $scope['taxonomies'] = ['category']; } return $scope; } diff --git a/src/AKSRT/Admin/Native/PayMenus.php b/src/AKSRT/Admin/Native/PayMenus.php index 7394a987..b5dbe027 100644 --- a/src/AKSRT/Admin/Native/PayMenus.php +++ b/src/AKSRT/Admin/Native/PayMenus.php @@ -74,28 +74,6 @@ class PayMenus { wp_die(esc_html__('下载记录模块未初始化。', 'aksrt')); }, ], - 'shipping' => [ - 'slug' => 'aksrt_shipping', - 'title' => __('发货及物流', 'aksrt'), - 'callback' => static function (): void { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - PayShipping::get_instance()->render_shipping_page(); - return; - } - wp_die(esc_html__('物流模块未初始化。', 'aksrt')); - }, - ], - 'after_sale' => [ - 'slug' => 'aksrt_after_sale', - 'title' => __('售后管理', 'aksrt'), - 'callback' => static function (): void { - if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) { - PayAfterSale::get_instance()->render_after_sale_page(); - return; - } - wp_die(esc_html__('售后模块未初始化。', 'aksrt')); - }, - ], 'coupons' => [ 'slug' => 'aksrt_coupons', 'title' => __('优惠码', 'aksrt'), @@ -203,23 +181,6 @@ class PayMenus { echo ''; } } - - if ($page !== 'aksrt_after_sale' && class_exists('\AKSRT\Payment\AfterSale')) { - $as_stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats(); - $pending_as = (int) ($as_stats['pending'] ?? 0); - if ($pending_as > 0) { - $url = admin_url('admin.php?page=aksrt_after_sale&status=pending'); - echo '
'; - echo '

' . esc_html__('售后工单待处理', 'aksrt') . '

'; - echo '

' . esc_html(sprintf( - /* translators: %d: pending after-sale count */ - __('您有 %d 个售后工单待处理', 'aksrt'), - $pending_as - )) . '

'; - echo '

' . esc_html__('去处理', 'aksrt') . '

'; - echo '
'; - } - } } private function ensure_cap(): void { @@ -255,17 +216,12 @@ class PayMenus { ? \AKSRT\Payment\ChargeCard::get_instance()->get_stats() : []; - echo '

' . esc_html__('运营数据入口(对齐 zibll 商城中心)。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '

'; + echo '

' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '

'; echo '
'; - $pending_ship = 0; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $pending_ship = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1])['total'] ?? 0); - } $this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders')); $this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending'])); $this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid'])); - $this->stat_card(__('待发货', 'aksrt'), (string) $pending_ship, $this->page_url('aksrt_shipping', ['view' => 'pending'])); $this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders')); $this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending'])); $this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active'])); diff --git a/src/AKSRT/Admin/Native/PayOrders.php b/src/AKSRT/Admin/Native/PayOrders.php index 338b1e62..63a1db47 100644 --- a/src/AKSRT/Admin/Native/PayOrders.php +++ b/src/AKSRT/Admin/Native/PayOrders.php @@ -92,7 +92,6 @@ class PayOrders { 'vip' => __('VIP', 'aksrt'), 'recharge' => __('充值', 'aksrt'), 'shop_product' => __('商城商品', 'aksrt'), - 'plate_follow' => __('版块关注', 'aksrt'), ]; echo '
    '; $tab_bits = []; @@ -289,13 +288,6 @@ class PayOrders { $admin_note = (string) $order['note']; } - $shipping_items = class_exists('\AKSRT\Payment\Shipping') - ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) - : []; - $after_sale = class_exists('\AKSRT\Payment\AfterSale') - ? (\AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20])['items'] ?? []) - : []; - echo '

    ← ' . esc_html__('返回订单列表', 'aksrt') . '

    '; echo ''; @@ -345,87 +337,6 @@ class PayOrders { echo ' '; submit_button(__('保存状态', 'aksrt'), 'primary', '', false); echo ''; - - echo '

    ' . esc_html__('物流记录', 'aksrt') . '

    '; - echo ''; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - echo '

    '; - echo '

    '; - echo '

    '; - submit_button(__('创建发货', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('物流管理', 'aksrt') . ''; - echo ''; - if (!$shipping_items) { - echo '

    ' . esc_html__('暂无物流记录。', 'aksrt') . '

    '; - } else { - $status_options = $this->shipping_status_options(false); - echo '
    '; - foreach ($shipping_items as $item) { - $sid = (int) ($item['id'] ?? 0); - $st = (string) ($item['status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
    ID' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html((string) $sid) . '' . esc_html((string) ($item['carrier'] ?? '')) . '' . esc_html((string) ($item['tracking_no'] ?? '')) . '' . esc_html($status_options[$st] ?? $st) . '' . esc_html((string) ($item['created_at'] ?? '')) . '
    '; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_shipping_' . $sid); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
    '; - } - - echo '

    ' . esc_html__('售后工单', 'aksrt') . '

    '; - echo '
    '; - echo ''; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number); - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - submit_button(__('创建售后', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('售后管理', 'aksrt') . ''; - echo '
    '; - if (!$after_sale) { - echo '

    ' . esc_html__('暂无售后工单。', 'aksrt') . '

    '; - } else { - echo ''; - foreach ($after_sale as $item) { - $tid = (int) ($item['id'] ?? 0); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
    ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html((string) $tid) . '' . esc_html((string) ($item['type_label'] ?? $item['type'] ?? '')) . '' . esc_html((string) ($item['status_label'] ?? $item['status'] ?? '')) . '' . esc_html((string) ($item['created_at'] ?? '')) . '' . esc_html__('在售后页处理', 'aksrt') . '
    '; - } } public function handle_update_order_status(): void { @@ -511,30 +422,6 @@ class PayOrders { exit; } - private function shipping_status_options(bool $include_all = false): array { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - return PayShipping::get_instance()->shipping_status_options_public($include_all); - } - return $include_all ? ['' => __('全部状态', 'aksrt')] : []; - } - - private function after_sale_type_options(bool $include_all = false): array { - if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) { - return PayAfterSale::get_instance()->after_sale_type_options_public($include_all); - } - return $include_all ? ['' => __('全部类型', 'aksrt')] : [ - 'refund' => __('仅退款', 'aksrt'), - ]; - } - - private function render_carrier_field(string $selected = ''): void { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - PayShipping::get_instance()->render_carrier_field_public($selected); - return; - } - echo ''; - } - private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void { $actions = [ 'completed' => __('完成', 'aksrt'), diff --git a/src/AKSRT/Admin/Native/PayShipping.php b/src/AKSRT/Admin/Native/PayShipping.php deleted file mode 100644 index caebf180..00000000 --- a/src/AKSRT/Admin/Native/PayShipping.php +++ /dev/null @@ -1,507 +0,0 @@ -ensure_cap(); - $this->render_header(__('发货及物流', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Shipping')) { - echo '

    ' . esc_html__('物流模块未初始化。', 'aksrt') . '

    '; - $this->render_footer(); - return; - } - - $view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records'; - if (!in_array($view, ['records', 'pending'], true)) { - $view = 'records'; - } - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = \AKSRT\Payment\Shipping::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->shipping_status_options(true); - - $pending_total = 0; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $pending_total = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0); - } - - echo '

    ' . esc_html(sprintf( - /* translators: 1: shipping records 2: pending shipment orders */ - __('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - $pending_total - )) . '

    '; - - echo ''; - - if ($view === 'pending') { - $this->render_pending_shipment_board($search, $page_num, $per_page); - $this->render_footer(); - return; - } - - echo '

    ' . esc_html__('新建发货', 'aksrt') . '

    '; - echo '
    '; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping'); - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - submit_button(__('创建发货', 'aksrt'), 'primary', '', false); - echo '
    '; - - echo '
    '; - echo ''; - echo ''; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
    '; - - $args = [ - 'status' => $status, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) { - $args['order_number'] = $search; - } else { - $args['tracking_no'] = $search; - } - } - $result = $svc->get_list($args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->shipping_status_options(false); - $show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0; - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - if ($show_express_id === $id) { - echo ''; - } - } - echo '
    ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html__('暂无物流记录', 'aksrt') . '
    ' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['carrier'] ?? '')) . '' . esc_html((string) ($row['tracking_no'] ?? '')) . '' . esc_html($row_status_options[$st] ?? $st) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - echo '
    '; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_shipping_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
    '; - echo '
    '; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_query_express_' . $id); - submit_button(__('查物流', 'aksrt'), 'small', '', false); - echo '
    '; - echo '' . esc_html__('轨迹', 'aksrt') . ''; - echo '
    '; - $this->render_express_timeline_panel($id); - echo '
    '; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void { - if (!class_exists('\AKSRT\Payment\OrderRepository')) { - echo '

    ' . esc_html__('订单服务不可用。', 'aksrt') . '

    '; - return; - } - - echo '
    '; - echo ''; - echo ''; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
    '; - - $result = \AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders([ - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ($result['max_pages'] ?? 1)); - $labels = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::order_status_labels() - : []; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - - if (!$items) { - echo ''; - } - - foreach ($items as $row) { - $order_number = (string) ($row['order_number'] ?? ''); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $status_key = (string) ($row['status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
    ' . esc_html__('订单号', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('金额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('支付时间', 'aksrt') . '' . esc_html__('立即发货', 'aksrt') . '
    ' . esc_html__('暂无待发货订单', 'aksrt') . '
    ' . esc_html($order_number) . '' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '' . esc_html((string) ($row['product_type'] ?? '')) . '' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '' . esc_html($labels[$status_key] ?? $status_key) . '' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . ''; - echo '
    '; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - $this->render_carrier_field(''); - echo ''; - echo ''; - submit_button(__('发货', 'aksrt'), 'primary small', '', false); - echo '
    '; - echo '
    '; - $this->render_pagination($pages, $page_num); - } - - public function handle_update_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_shipping_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Shipping')) { - $ok = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id()); - } - - $redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : ''; - if ($redirect_order !== '') { - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $redirect_order, - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - public function handle_query_express(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $force = !empty($_POST['force']); - check_admin_referer('aksrt_pay_query_express_' . $id); - - $ok = false; - $order_number = ''; - if ($id > 0 && class_exists('\AKSRT\Payment\Shipping')) { - $svc = \AKSRT\Payment\Shipping::get_instance(); - $record = $svc->get($id); - $order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : ''; - $result = $svc->query_express($id, $force); - $ok = !empty($result['success']); - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'express_id' => $id, - 's' => $order_number, - 'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed', - ])); - exit; - } - - public function handle_create_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - $redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order'; - $nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number); - check_admin_referer($nonce_action); - - $ok = false; - if ($order_number !== '' && class_exists('\AKSRT\Payment\Shipping')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - $this->redirect_shipping_create_result(false, $order_number, $redirect_page); - } - } - $id = \AKSRT\Payment\Shipping::get_instance()->create([ - 'order_number' => $order_number, - 'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')), - 'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - 'operator_id' => get_current_user_id(), - ]); - $ok = $id > 0; - } - - $this->redirect_shipping_create_result($ok, $order_number, $redirect_page); - } - - private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void { - $msg = $ok ? 'shipping_created' : 'shipping_create_failed'; - if ($redirect_page === 'shipping') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $msg, - 's' => $order_number, - ])); - exit; - } - if ($redirect_page === 'pending') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'pending', - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - - public function shipping_status_options_public(bool $include_all = false): array { - return $this->shipping_status_options($include_all); - } - - private function shipping_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); - } - if (class_exists('\AKSRT\Payment\Shipping')) { - foreach (\AKSRT\Payment\Shipping::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - if (count($options) === (int) $include_all) { - $options = array_merge($options, [ - 'shipped' => __('已发货', 'aksrt'), - 'in_transit' => __('运输中', 'aksrt'), - 'delivered' => __('已签收', 'aksrt'), - 'returned' => __('已退回', 'aksrt'), - ]); - } - return $options; - } - public function render_carrier_field_public(string $selected = ''): void { - $this->render_carrier_field($selected); - } - - private function render_carrier_field(string $selected = ''): void { - $options = []; - if (class_exists('\AKSRT\Payment\Shipping')) { - foreach (\AKSRT\Payment\Shipping::get_carrier_options() as $item) { - $value = (string) ($item['value'] ?? ''); - $label = (string) ($item['label'] ?? $value); - if ($value !== '') { - $options[$value] = $label; - } - } - } - if ($options) { - echo ''; - return; - } - echo ''; - } - private function render_express_timeline_panel(int $shipping_id): void { - if ($shipping_id <= 0 || !class_exists('\AKSRT\Payment\Shipping')) { - echo '

    ' . esc_html__('无法加载物流轨迹。', 'aksrt') . '

    '; - return; - } - $svc = \AKSRT\Payment\Shipping::get_instance(); - // Prefer cached data; do not force remote query on pure view. - $timeline = $svc->build_express_timeline($shipping_id); - $record = $svc->get($shipping_id); - echo '
    '; - echo '

    ' . esc_html__('物流轨迹', 'aksrt') . ' · '; - echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? '')); - echo '

    '; - if (!$timeline) { - echo '

    ' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '

    '; - echo '
    '; - return; - } - echo '
      '; - // newest first for operators - $timeline = array_reverse($timeline); - foreach ($timeline as $item) { - $time = (string) ($item['time'] ?? ''); - $label = (string) ($item['label'] ?? ''); - echo '
    1. ' . esc_html($time) . ' ' . esc_html($label) . '
    2. '; - } - echo '
'; - } - - - private function ensure_cap(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); - } - } - - private function render_header(string $title): void { - echo '
'; - echo '

' . esc_html($title) . '

'; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
'; - } - - private function render_flash(): void { - if (empty($_GET['aksrt_pay_msg'])) { - return; - } - $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); - $map = [ - 'shipping_updated' => __('物流状态已更新。', 'aksrt'), - 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), - 'shipping_created' => __('发货记录已创建。', 'aksrt'), - 'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'), - 'express_ok' => __('物流轨迹已更新。', 'aksrt'), - 'express_failed' => __('物流轨迹查询失败。', 'aksrt'), - ]; - if (!isset($map[$code])) { - return; - } - $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; - echo '

' . esc_html($map[$code]) . '

'; - } - - private function page_url(string $slug, array $args = []): string { - return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); - } - - private function render_pagination(int $pages, int $page_num): void { - if ($pages <= 1) { - return; - } - echo '
'; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
'; - } -} diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php index 8741fc7f..7d3d9f6b 100644 --- a/src/AKSRT/Admin/Native/UsersMenus.php +++ b/src/AKSRT/Admin/Native/UsersMenus.php @@ -573,7 +573,7 @@ class UsersMenus { } echo '

'; echo '

'; diff --git a/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php b/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php index 31792fb4..f305441b 100644 --- a/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php +++ b/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php @@ -28,7 +28,6 @@ class SchemaExtensionsAudit { ['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' => [ diff --git a/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php b/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php index 95521428..2a9a68f4 100644 --- a/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php +++ b/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php @@ -81,9 +81,6 @@ class SchemaExtensionsSecurity { ['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/src/AKSRT/Admin/Schema/SchemaForum.php b/src/AKSRT/Admin/Schema/SchemaForum.php deleted file mode 100644 index 834da7d8..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForum.php +++ /dev/null @@ -1,126 +0,0 @@ - 'forum', - 'title' => '社区&论坛', - 'sections' => [ - 'basic' => SchemaForumBasic::build(), - 'names' => SchemaForumNames::build(), - 'homepage' => SchemaForumHomepage::build(), - 'board_page' => SchemaForumBoardPage::build(), - 'post_page' => SchemaForumPostPage::build(), - 'reply' => SchemaForumReply::build(), - 'permissions' => SchemaForumPermissions::build(), - 'upload' => SchemaForumUpload::build(), - 'credits' => SchemaForumCredits::build(), - 'other' => SchemaForumOther::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\Settings')) { - return []; - } - - $defs = \AKSRT\Settings\Settings::cap_definitions(); - $cols = \AKSRT\Settings\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/src/AKSRT/Admin/Schema/SchemaForumBasic.php b/src/AKSRT/Admin/Schema/SchemaForumBasic.php deleted file mode 100644 index 8e554178..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumBasic.php +++ /dev/null @@ -1,28 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php b/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php deleted file mode 100644 index 6b36f487..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php +++ /dev/null @@ -1,34 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaForumCredits.php b/src/AKSRT/Admin/Schema/SchemaForumCredits.php deleted file mode 100644 index 823e021b..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumCredits.php +++ /dev/null @@ -1,34 +0,0 @@ - '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_hot_recommend_threshold', 'label' => '热帖推荐分阈值', 'type' => 'number', 'default' => 500, 'min' => 1, 'help' => '推荐分达到该值触发热帖奖励与勋章累计'], - ['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/src/AKSRT/Admin/Schema/SchemaForumHomepage.php b/src/AKSRT/Admin/Schema/SchemaForumHomepage.php deleted file mode 100644 index 8621d7a0..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumHomepage.php +++ /dev/null @@ -1,32 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaForumNames.php b/src/AKSRT/Admin/Schema/SchemaForumNames.php deleted file mode 100644 index d8622ba3..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumNames.php +++ /dev/null @@ -1,36 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaForumOther.php b/src/AKSRT/Admin/Schema/SchemaForumOther.php deleted file mode 100644 index ab10ff8f..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumOther.php +++ /dev/null @@ -1,46 +0,0 @@ - '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' => true], - ['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/src/AKSRT/Admin/Schema/SchemaForumPermissions.php b/src/AKSRT/Admin/Schema/SchemaForumPermissions.php deleted file mode 100644 index 861de46d..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumPermissions.php +++ /dev/null @@ -1,46 +0,0 @@ - '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' => true], - ['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' => true], - ['key' => 'bbs_post_pay_vip_price_s', 'label' => 'VIP 特价', 'type' => 'switch', 'default' => true], - ['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/src/AKSRT/Admin/Schema/SchemaForumPostPage.php b/src/AKSRT/Admin/Schema/SchemaForumPostPage.php deleted file mode 100644 index fed7afbe..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumPostPage.php +++ /dev/null @@ -1,46 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaForumReply.php b/src/AKSRT/Admin/Schema/SchemaForumReply.php deleted file mode 100644 index 4094a29a..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumReply.php +++ /dev/null @@ -1,39 +0,0 @@ - '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' => true], - ['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' => true], - ['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/src/AKSRT/Admin/Schema/SchemaForumUpload.php b/src/AKSRT/Admin/Schema/SchemaForumUpload.php deleted file mode 100644 index 02938bec..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumUpload.php +++ /dev/null @@ -1,29 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaPageDisplay.php b/src/AKSRT/Admin/Schema/SchemaPageDisplay.php index 390a0b6c..a3a8442f 100644 --- a/src/AKSRT/Admin/Schema/SchemaPageDisplay.php +++ b/src/AKSRT/Admin/Schema/SchemaPageDisplay.php @@ -98,9 +98,6 @@ class SchemaPageDisplay { ['label' => '用户页', 'value' => 'author'], ['label' => '文章页', 'value' => 'single'], ['label' => '搜索页', 'value' => 'search'], - ['label' => '[论坛]首页', 'value' => 'forum_home'], - ['label' => '[论坛]板块页面', 'value' => 'forum_plate'], - ['label' => '[论坛]帖子页面', 'value' => 'forum_post'], ['label' => '其它页面', 'value' => 'page'], ]], ['key' => 'header_components_device_visibility', 'label' => '设备显示规则', 'type' => 'radio', 'default' => '', 'options' => [ @@ -255,7 +252,7 @@ class SchemaPageDisplay { ['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,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum'], + ['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits'], ['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,favorites,comments,follow'], ['key' => 'user_home_tabs_pc_hide', 'label' => 'PC 端隐藏', 'type' => 'input', 'default' => ''], ['key' => 'user_home_tabs_mobile_hide', 'label' => '移动端隐藏', 'type' => 'input', 'default' => ''], diff --git a/src/AKSRT/Admin/Schema/SchemaShop.php b/src/AKSRT/Admin/Schema/SchemaShop.php deleted file mode 100644 index 838d1577..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShop.php +++ /dev/null @@ -1,28 +0,0 @@ - 'shop', - 'title' => '商城&商品', - 'version' => '1.0.0', - 'sections' => [ - 'basic' => SchemaShopBasic::build(), - 'list' => SchemaShopList::build(), - 'contact' => SchemaShopContact::build(), - 'single' => SchemaShopSingle::build(), - 'product-defaults' => SchemaShopProductDefaults::build(), - 'shipping-config' => SchemaShopShippingConfig::build(), - 'timing' => SchemaShopTiming::build(), - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopBasic.php b/src/AKSRT/Admin/Schema/SchemaShopBasic.php deleted file mode 100644 index 91f2d1ed..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopBasic.php +++ /dev/null @@ -1,37 +0,0 @@ - 'basic', - 'title' => '商城基础', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'general', 'title' => '商城开关', 'fields' => [ - ['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => true], - ['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' => false], - ['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' => true], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopContact.php b/src/AKSRT/Admin/Schema/SchemaShopContact.php deleted file mode 100644 index e6e19563..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopContact.php +++ /dev/null @@ -1,30 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaShopList.php b/src/AKSRT/Admin/Schema/SchemaShopList.php deleted file mode 100644 index da6c4c58..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopList.php +++ /dev/null @@ -1,51 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php b/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php deleted file mode 100644 index d3700878..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php +++ /dev/null @@ -1,46 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php b/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php deleted file mode 100644 index 7df3b1b9..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php +++ /dev/null @@ -1,41 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaShopSingle.php b/src/AKSRT/Admin/Schema/SchemaShopSingle.php deleted file mode 100644 index b7b92837..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopSingle.php +++ /dev/null @@ -1,39 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaShopTiming.php b/src/AKSRT/Admin/Schema/SchemaShopTiming.php deleted file mode 100644 index e6f5e10d..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopTiming.php +++ /dev/null @@ -1,38 +0,0 @@ - '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/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php index 2705a9da..26732467 100644 --- a/src/AKSRT/Admin/Schema/SchemaUsers.php +++ b/src/AKSRT/Admin/Schema/SchemaUsers.php @@ -23,10 +23,7 @@ class SchemaUsers { return [ 'day_max' => 100, 'sign_up' => 20, 'sign_in' => 5, 'followed' => 5, 'post_new' => 5, 'post_like' => 1, 'post_favorite' => 2, - 'comment_new' => 2, 'comment_like' => 1, 'bbs_posts_new' => 3, - 'bbs_score_extra' => 1, 'bbs_essence' => 2, 'bbs_posts_hot' => 2, - 'bbs_plate_new' => 2, 'bbs_plate_hot' => 2, 'bbs_adopt' => 2, - 'bbs_comment_hot' => 2, + 'comment_new' => 2, 'comment_like' => 1, ]; } @@ -459,13 +456,6 @@ class SchemaUsers { ['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], ['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], ['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], - ['key' => 'points_free_bbs_posts_new', 'label' => '论坛发帖', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_reply', 'label' => '论坛回复', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_essence', 'label' => '帖子加精', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_adopt', 'label' => '回复被采纳', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_posts_hot', 'label' => '热帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_comment_hot', 'label' => '神评奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_score_extra', 'label' => '帖子加分', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 1000], ['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'], ]], ['key' => 'spend', 'title' => '积分消费', 'fields' => [ diff --git a/src/AKSRT/Admin/SchemaProvider.php b/src/AKSRT/Admin/SchemaProvider.php index cd5a2a4a..c8ff8c06 100644 --- a/src/AKSRT/Admin/SchemaProvider.php +++ b/src/AKSRT/Admin/SchemaProvider.php @@ -43,8 +43,6 @@ class SchemaProvider { 'payment', 'extensions', 'tools', - 'shop', - 'forum', ]; } @@ -92,8 +90,6 @@ class SchemaProvider { 'payment' => 'aksrt_settings', 'extensions' => 'aksrt_settings', 'tools' => 'aksrt_settings', - 'shop' => 'aksrt_settings', - 'forum' => 'aksrt_settings', ]; return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings'; @@ -275,8 +271,6 @@ class SchemaProvider { 'payment' => Schema\SchemaPayment::class, 'extensions' => Schema\SchemaExtensions::class, 'tools' => Schema\SchemaTools::class, - 'shop' => Schema\SchemaShop::class, - 'forum' => Schema\SchemaForum::class, ]; } diff --git a/src/AKSRT/Admin/ShopController.php b/src/AKSRT/Admin/ShopController.php deleted file mode 100644 index 4d9ca529..00000000 --- a/src/AKSRT/Admin/ShopController.php +++ /dev/null @@ -1,453 +0,0 @@ -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'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_after_sales'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P\d+)/return-shipping', [ - 'methods' => 'POST', - 'callback' => [$this, 'submit_frontend_return_shipping'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P\d+)/cancel', [ - 'methods' => 'POST', - 'callback' => [$this, 'cancel_frontend_after_sale'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/coupons', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_coupons'], - '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 taxonomy options used by the product editor. - register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_product_taxonomy'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Create product category or tag term for the product editor. - register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_product_taxonomy_term'], - '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/frontend/orders/(?P[A-Za-z0-9_-]+)/rebuy', [ - 'methods' => 'POST', - 'callback' => [$this, 'rebuy_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_product_archive'], - 'permission_callback' => '__return_true', - ]); - - // Product list widget AJAX (zibll ajax_widget_ui product lists). - register_rest_route(self::API_NAMESPACE, '/shop/frontend/widgets/products', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_widget_products'], - 'permission_callback' => '__return_true', - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_revenue'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_withdraw'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/review', [ - 'methods' => 'POST', - 'callback' => [$this, 'submit_frontend_product_review'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/review-modal', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_product_review_modal'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - -register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P\d+)/favorite', [ - 'methods' => 'POST', - 'callback' => [$this, 'toggle_frontend_product_favorite'], - '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/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php b/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php deleted file mode 100644 index 50ca7b57..00000000 --- a/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php +++ /dev/null @@ -1,97 +0,0 @@ -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\Payment\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\Payment\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\Payment\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\Payment\Coupon')) { - return $this->error('优惠券系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $success = \AKSRT\Payment\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/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php deleted file mode 100644 index 4e50cac9..00000000 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ /dev/null @@ -1,717 +0,0 @@ -error('商城服务不可用', 500); - } - - $result = \AKSRT\Shop\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\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $result = \AKSRT\Shop\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 { - $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')); - $product_type = sanitize_key((string) $request->get_param('product_type')); - - $args = [ - 'page' => $page, - 'per_page' => $per_page, - 'status' => $status, - 'product_type' => $product_type, - ]; - - if (class_exists('\AKSRT\Payment\UserOrders')) { - $view = \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $args); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), $args); - } else { - $view = ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]; - } - - return $this->success(['data' => $view]); - } - - public function get_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $order = null; - if (class_exists('\AKSRT\Payment\UserOrders')) { - $order = \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $order = \AKSRT\Shop\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 { - $params = $this->get_json_params($request); - $payment_method = sanitize_key((string) ($params['payment_method'] ?? '')); - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - - if (class_exists('\AKSRT\Payment\UserOrders')) { - $result = \AKSRT\Payment\UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method ?: 'balance'); - } else { - return $this->error('订单服务不可用', 500); - } - 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 { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - if (class_exists('\AKSRT\Payment\UserOrders')) { - $result = \AKSRT\Payment\UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number); - } else { - return $this->error('订单服务不可用', 500); - } - 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\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\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_frontend_after_sales(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $data = \AKSRT\Shop\Shop::get_instance()->get_user_after_sales(get_current_user_id()); - return $this->success(['data' => $data]); - } - - public function submit_frontend_return_shipping(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $result = \AKSRT\Shop\Shop::get_instance()->submit_after_sale_return(get_current_user_id(), $id, $params); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '提交退货物流失败'); - } - return $this->success([ - 'message' => $result['message'] ?? __('退货物流已提交', 'aksrt'), - 'data' => $result['data'] ?? [], - ]); - } - - public function cancel_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $reason = sanitize_text_field((string) ($params['reason'] ?? '')); - $result = \AKSRT\Shop\Shop::get_instance()->cancel_after_sale(get_current_user_id(), $id, $reason); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '取消售后失败'); - } - return $this->success(['message' => $result['message'] ?? __('已取消', 'aksrt')]); - } - - /** - * Active public coupons for wallet UI (not personal inventory — site-wide active codes). - */ - public function get_frontend_coupons(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Coupon')) { - return $this->error('优惠券不可用', 500); - } - $list = \AKSRT\Payment\Coupon::get_instance()->get_list([ - 'status' => 'active', - 'limit' => 50, - 'offset' => 0, - ]); - $user_id = get_current_user_id(); - $items = []; - foreach ((array) ($list['items'] ?? []) as $c) { - $items[] = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? ''), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'min_amount' => (float) ($c['min_amount'] ?? 0), - 'max_discount' => (float) ($c['max_discount'] ?? 0), - 'expires_at' => (string) ($c['expires_at'] ?? ''), - 'user_used' => $user_id > 0 - ? (int) \AKSRT\Payment\Coupon::get_instance()->get_user_usage_count((int) ($c['id'] ?? 0), $user_id) - : 0, - 'per_user_limit' => (int) ($c['per_user_limit'] ?? 0), - ]; - } - return $this->success(['data' => ['items' => $items]]); - } - - public function get_cart(\WP_REST_Request $request): \WP_REST_Response { - $user_id = get_current_user_id(); - $shop = \AKSRT\Shop\Shop::get_instance(); - $cart = $shop->get_cart($user_id); - - $items = []; - foreach ($cart as $pid => $opts_map) { - $product_id = (int) $pid; - $product = $shop->build_product_card_view($product_id); - if (empty($product) || !is_array($opts_map)) { - continue; - } - foreach ($opts_map as $opt_str => $qty) { - $options_string = $opt_str === '_default' ? '' : (string) $opt_str; - $unit_price = method_exists($shop, 'resolve_option_price') - ? (float) $shop->resolve_option_price($product_id, $options_string !== '' ? $options_string : '_default') - : (float) ($product['price'] ?? 0); - $options_label = method_exists($shop, 'resolve_option_labels') - ? (string) $shop->resolve_option_labels($product_id, $options_string !== '' ? $options_string : '_default') - : ''; - $option_image = method_exists($shop, 'get_option_image') - ? (string) $shop->get_option_image($product_id, $options_string !== '' ? $options_string : '_default') - : ''; - $stock = method_exists($shop, 'resolve_option_stock') - ? (int) $shop->resolve_option_stock($product_id, $options_string !== '' ? $options_string : '_default') - : (int) ($product['stock'] ?? 0); - - $items[] = [ - 'product_id' => $product_id, - 'options_string' => $options_string, - 'options_label' => $options_label, - 'quantity' => (int) $qty, - 'unit_price' => $unit_price, - 'line_total' => round($unit_price * (int) $qty, 2), - 'stock' => $stock, - 'image' => $option_image !== '' ? $option_image : (string) ($product['image'] ?? ''), - 'title' => (string) ($product['title'] ?? ''), - '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\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\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\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\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\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\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 toggle_frontend_product_favorite(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $product_id = (int) $request->get_param('id'); - $result = \AKSRT\Shop\Shop::get_instance()->toggle_favorite(get_current_user_id(), $product_id); - if (empty($result['success'])) { - return $this->error(__('收藏操作失败', 'aksrt')); - } - - return $this->success([ - 'message' => !empty($result['is_favorite']) ? __('已收藏', 'aksrt') : __('已取消收藏', 'aksrt'), - 'data' => $result, - ]); - } - - public function rebuy_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->re_purchase(get_current_user_id(), $order_number); - if (empty($result['success'])) { - return $this->error($result['message'] ?? __('再次购买失败', 'aksrt')); - } - - return $this->success([ - 'message' => __('商品已加入购物车', 'aksrt'), - 'data' => $result, - ]); - } - - public function get_frontend_addresses(): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->success(['data' => ['addresses' => []]]); - } - - $addresses = \AKSRT\Shop\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\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $params = $this->get_json_params($request); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\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\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\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\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $address_id = (int) $request->get_param('id'); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\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\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $address_id = (int) $request->get_param('id'); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\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 get_frontend_product_archive(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? '')); - $term_id = (int) ($request->get_param('term_id') ?? 0); - $page = max(1, (int) ($request->get_param('page') ?? 1)); - $orderby = sanitize_key((string) ($request->get_param('orderby') ?? '')); - $order = sanitize_key((string) ($request->get_param('order') ?? '')); - - $view = \AKSRT\Shop\Shop::get_instance()->get_product_archive_view([ - 'taxonomy' => $taxonomy, - 'term_id' => $term_id, - 'paged' => $page, - 'orderby' => $orderby, - 'order' => $order, - ]); - - $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; - $html = ''; - if (!empty($view['products']) && is_array($view['products'])) { - ob_start(); - foreach ($view['products'] as $product) { - if (function_exists('get_template_part')) { - get_template_part('templates/parts/shop/product', 'card', [ - 'product' => $product, - 'sales_cfg' => $sales_cfg, - ]); - } - } - $html = (string) ob_get_clean(); - } - - $empty_html = ''; - if ($html === '' && function_exists('aksrt_get_ajax_null')) { - $empty_html = aksrt_get_ajax_null(__('内容空空如也', 'aksrt')); - } elseif ($html === '') { - $empty_html = '

' . esc_html__('内容空空如也', 'aksrt') . '

'; - } - - // Header HTML for cat filter replace (when filter present). - $header_html = ''; - if (!empty($view['filter']) && is_array($view['filter'])) { - ob_start(); - $this->render_product_cat_filter_html($view); - $header_html = (string) ob_get_clean(); - } - - $paginate_html = ''; - $page_n = (int) ($view['page'] ?? 1); - $max_pages = (int) ($view['max_pages'] ?? 1); - $has_more = $page_n < $max_pages; - if ($has_more && (string) ($view['paging'] ?? '') === 'ajax') { - $next_url = add_query_arg([ - 'taxonomy' => $taxonomy, - 'term_id' => $term_id, - 'page' => $page_n + 1, - 'orderby' => (string) ($view['orderby'] ?? 'date'), - 'order' => (string) ($view['order'] ?? ''), - ], rest_url('aksrt/v1/shop/frontend/products/archive')); - if (function_exists('aksrt_get_ajax_next_paginate')) { - $paginate_html = aksrt_get_ajax_next_paginate( - (int) ($view['total'] ?? 0), - $page_n, - max(1, (int) ($view['per_page'] ?? 12)), - $next_url - ); - } - } - - return $this->success([ - 'data' => array_merge($view, [ - 'html' => $html !== '' ? $html : $empty_html, - 'empty_html' => $empty_html, - 'header_html' => $header_html, - 'paginate_html' => $paginate_html, - 'has_more' => $has_more, - 'title' => !empty($view['term']['name']) - ? (string) $view['term']['name'] . ' - ' . wp_strip_all_tags(get_bloginfo('name')) - : '', - ]), - ]); - } - - /** - * GET /shop/frontend/widgets/products — product list widget AJAX pager. - */ - public function get_frontend_widget_products(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Widgets\WidgetProductList')) { - return $this->error('商品组件不可用', 500); - } - - $instance = [ - 'per_page' => max(1, min(50, (int) ($request->get_param('per_page') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), - 'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')), - 'cat_ids' => sanitize_text_field((string) ($request->get_param('cat_ids') ?? '')), - 'tag_ids' => sanitize_text_field((string) ($request->get_param('tag_ids') ?? '')), - 'discount_ids' => sanitize_text_field((string) ($request->get_param('discount_ids') ?? '')), - 'exclude_cat' => !empty($request->get_param('exclude_cat')) ? 1 : 0, - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')), - 'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), - ]; - $page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1)); - $payload = \AKSRT\Widgets\WidgetProductList::query_payload($instance, $page); - - return $this->success([ - 'html' => (string) ($payload['html'] ?? ''), - 'paginate_html' => (string) ($payload['paginate_html'] ?? ''), - 'has_more' => !empty($payload['has_more']), - 'page' => (int) ($payload['page'] ?? $page), - 'max_pages' => (int) ($payload['max_pages'] ?? 1), - 'total' => (int) ($payload['total'] ?? 0), - ]); - } - - /** - * Render cat filter bar HTML for AJAX header replace. - */ - private function render_product_cat_filter_html(array $view): void { - $filter = is_array($view['filter'] ?? null) ? $view['filter'] : []; - $orderby = (string) ($view['orderby'] ?? ($filter['orderby'] ?? 'date')); - $order = strtoupper((string) ($view['order'] ?? ($filter['order'] ?? 'DESC'))); - $term = is_array($view['term'] ?? null) ? $view['term'] : []; - $current_id = (int) ($filter['current_id'] ?? ($term['id'] ?? 0)); - include get_template_directory() . '/templates/parts/shop/product-cat-filter.php'; - } - - public function get_frontend_revenue(\WP_REST_Request $request): \WP_REST_Response { - if (!function_exists('aksrt_get_revenue_page_view')) { - return $this->error('收益服务不可用', 500); - } - $view = aksrt_get_revenue_page_view(get_current_user_id()); - $withdrawals = []; - if (class_exists('\AKSRT\Payment\Withdrawal')) { - $rows = \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]); - foreach ($rows as $row) { - $withdrawals[] = [ - 'id' => (int) ($row['id'] ?? 0), - 'amount' => (float) ($row['amount'] ?? 0), - 'status' => (string) ($row['status'] ?? ''), - 'method' => (string) ($row['method'] ?? ''), - 'created_at' => (string) ($row['created_at'] ?? ''), - 'note' => (string) ($row['note'] ?? ''), - ]; - } - } - $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : []; - return $this->success([ - 'data' => array_merge($view, [ - 'withdrawals' => $withdrawals, - 'settings' => [ - 'min_amount' => (float) ($settings['min_amount'] ?? 50), - 'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0), - 'text' => (string) ($settings['text'] ?? ''), - ], - ]), - ]); - } - - public function create_frontend_withdraw(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Withdrawal') || !class_exists('\AKSRT\Credits\Credits')) { - return $this->error('提现服务不可用', 500); - } - - $params = $this->get_json_params($request); - $amount = (float) ($params['amount'] ?? 0); - $method = sanitize_key((string) ($params['method'] ?? 'alipay')); - $account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? '')); - - $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : []; - $min = (float) ($settings['min_amount'] ?? 50); - if ($amount < $min) { - return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min)); - } - if ($account === '') { - return $this->error(__('请填写收款账户信息', 'aksrt')); - } - - $user_id = get_current_user_id(); - $balance = (float) \AKSRT\Credits\Credits::balance($user_id); - if ($amount > $balance) { - return $this->error(__('可提现余额不足', 'aksrt')); - } - - $cost = (int) round($amount); - if ($cost <= 0) { - return $this->error(__('提现金额无效', 'aksrt')); - } - $deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt')); - if (is_wp_error($deducted)) { - return $this->error($deducted->get_error_message()); - } - - $id = \AKSRT\Payment\Withdrawal::get_instance()->create($user_id, $amount, $method, $account); - if ($id <= 0) { - \AKSRT\Credits\Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt')); - return $this->error(__('提现申请创建失败', 'aksrt'), 500); - } - - return $this->success([ - 'message' => __('提现申请已提交', 'aksrt'), - 'data' => [ - 'id' => $id, - 'amount' => $amount, - 'balance' => (float) \AKSRT\Credits\Credits::balance($user_id), - ], - ], 201); - } - - public function submit_frontend_product_review(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\ProductReview')) { - return $this->error('评价服务不可用', 500); - } - $params = $this->get_json_params($request); - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $product_id = (int) ($params['product_id'] ?? 0); - $result = \AKSRT\Shop\ProductReview::get_instance()->submit_review( - get_current_user_id(), - $order_number, - $product_id, - $params - ); - if (empty($result['success'])) { - return $this->error($result['message'] ?? __('评价失败', 'aksrt')); - } - return $this->success([ - 'message' => __('评价成功', 'aksrt'), - 'data' => $result, - ], 201); - } - - /** - * GET review modal HTML for an order (zibll order_comment_modal parity). - */ - public function get_frontend_product_review_modal(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\ProductReview')) { - return $this->error(__('评价服务不可用', 'aksrt'), 500); - } - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\ProductReview::get_instance()->get_review_modal_html( - get_current_user_id(), - $order_number - ); - if (empty($result['success'])) { - return $this->error((string) ($result['message'] ?? __('无法打开评价', 'aksrt'))); - } - return $this->success([ - 'html' => (string) ($result['html'] ?? ''), - 'product_id' => (int) ($result['product_id'] ?? 0), - 'need_shipping' => !empty($result['need_shipping']), - 'img_max' => (int) ($result['img_max'] ?? 0), - 'remain_seconds' => (int) ($result['remain_seconds'] ?? 0), - ]); - } - - 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/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php b/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php deleted file mode 100644 index 6cdb1c20..00000000 --- a/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php +++ /dev/null @@ -1,353 +0,0 @@ -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\Payment\AfterSale::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\Payment\AfterSale::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\Payment\AfterSale::get_status_options(), - 'stage' => \AKSRT\Payment\AfterSale::get_stage_options(), - 'type' => \AKSRT\Payment\AfterSale::get_type_options(), - ], - ])]); - } - - /** - * POST /shop/after-sale — 创建售后工单 - */ - public function create_after_sale(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\AfterSale')) { - 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\Payment\OrderRepository') - ? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) - : null; - - if (!$order) { - return $this->error(__('订单不存在', 'aksrt'), 404); - } - - unset($params['user_id'], $params['applicant_user_id']); - $id = \AKSRT\Payment\AfterSale::get_instance()->create($params); - - if (is_wp_error($id)) { - return $this->error($id->get_error_message(), 422); - } - 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\Payment\AfterSale')) { - 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\Payment\AfterSale::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\Payment\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\Payment\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\Payment\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\Payment\Shipping::get_status_options(), - 'carrier' => \AKSRT\Payment\Shipping::get_carrier_options(), - ], - ])]); - } - - /** - * POST /shop/shipping — 创建发货记录 - */ - public function create_shipping(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\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\Payment\OrderRepository') - ? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) - : null; - - if (!$order) { - return $this->error(__('订单不存在', 'aksrt'), 404); - } - - $params['operator_id'] = get_current_user_id(); - - $id = \AKSRT\Payment\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\Payment\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\Payment\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\Payment\AfterSale')) { - return $this->error('售后系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $record = \AKSRT\Payment\AfterSale::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\Payment\Shipping')) { - return $this->error('物流系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $record = \AKSRT\Payment\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\Payment\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\Payment\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/src/AKSRT/Admin/ShopControllerOrderOperations.php b/src/AKSRT/Admin/ShopControllerOrderOperations.php deleted file mode 100644 index 18647018..00000000 --- a/src/AKSRT/Admin/ShopControllerOrderOperations.php +++ /dev/null @@ -1,136 +0,0 @@ -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\Payment\OrderRepository')) { - $repo = \AKSRT\Payment\OrderRepository::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\Payment\OrderRepository')) { - return $this->error('订单服务不可用', 500); - } - - $repo = \AKSRT\Payment\OrderRepository::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\Shop')) { - $shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : []; - $after_sale_result = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::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\Payment\OrderRepository')) { - $success = \AKSRT\Payment\OrderRepository::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/src/AKSRT/Admin/ShopControllerProductAdminColumns.php b/src/AKSRT/Admin/ShopControllerProductAdminColumns.php deleted file mode 100644 index 1472a11a..00000000 --- a/src/AKSRT/Admin/ShopControllerProductAdminColumns.php +++ /dev/null @@ -1,187 +0,0 @@ - $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, - ]); - ?> -
-
-

- - -
-
- 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\Payment\OrderRepository')) { - $order_stats = \AKSRT\Payment\OrderRepository::get_instance()->get_stats(); - } - - $revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0]; - if (class_exists('\AKSRT\Payment\Revenue')) { - $revenue_stats = \AKSRT\Payment\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, - ], - ]); - } - - public function get_product_taxonomy(): \WP_REST_Response { - return $this->success([ - 'data' => [ - 'categories' => $this->format_product_terms('aksrt_product_cat'), - 'tags' => $this->format_product_terms('aksrt_product_tag'), - ], - ]); - } - - /** - * POST /shop/products/taxonomy — create category or tag term. - * - * Body: { taxonomy: 'category'|'tag'|'aksrt_product_cat'|'aksrt_product_tag', name: string, parent?: int } - */ - public function create_product_taxonomy_term(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $raw_tax = sanitize_key((string) ($params['taxonomy'] ?? 'category')); - $name = sanitize_text_field((string) ($params['name'] ?? '')); - $parent = max(0, (int) ($params['parent'] ?? 0)); - - $taxonomy_map = [ - 'category' => 'aksrt_product_cat', - 'tag' => 'aksrt_product_tag', - 'aksrt_product_cat' => 'aksrt_product_cat', - 'aksrt_product_tag' => 'aksrt_product_tag', - ]; - $taxonomy = $taxonomy_map[$raw_tax] ?? ''; - - if ($taxonomy === '' || !taxonomy_exists($taxonomy)) { - return $this->error(__('无效的分类法', 'aksrt')); - } - if ($name === '') { - return $this->error(__('名称不能为空', 'aksrt')); - } - - $existing = term_exists($name, $taxonomy, $taxonomy === 'aksrt_product_cat' ? $parent : 0); - if (is_array($existing) && !empty($existing['term_id'])) { - $term = get_term((int) $existing['term_id'], $taxonomy); - if ($term && !is_wp_error($term)) { - return $this->success([ - 'message' => __('术语已存在', 'aksrt'), - 'data' => [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', - 'existing' => true, - ], - ]); - } - } - - $args = []; - if ($taxonomy === 'aksrt_product_cat' && $parent > 0) { - $args['parent'] = $parent; - } - - $result = wp_insert_term($name, $taxonomy, $args); - if (is_wp_error($result)) { - return $this->error($result->get_error_message()); - } - - $term_id = (int) ($result['term_id'] ?? 0); - $term = get_term($term_id, $taxonomy); - if (!$term || is_wp_error($term)) { - return $this->error(__('创建失败', 'aksrt')); - } - - return $this->success([ - 'message' => __('术语已创建', 'aksrt'), - 'data' => [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', - 'existing' => false, - ], - ], 201); - } - - /** - * @return array - */ - private function format_product_terms(string $taxonomy): array { - if (!taxonomy_exists($taxonomy)) { - return []; - } - - $terms = get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - ]); - if (is_wp_error($terms) || !is_array($terms)) { - return []; - } - - return array_map(static function (\WP_Term $term): array { - return [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'count' => (int) $term->count, - ]; - }, $terms); - } - - /** - * 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); - } - - if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) { - \AKSRT\Shop\Shop::get_instance()->save_product_options((int) $post_id, $params['options']); - } - - 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'])); - } - - if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) { - \AKSRT\Shop\Shop::get_instance()->save_product_options($id, $params['options']); - } - - return $this->success([ - 'message' => __('商品已更新', 'aksrt'), - ]); - } -} diff --git a/src/AKSRT/Admin/ShopControllerVariantExportOperations.php b/src/AKSRT/Admin/ShopControllerVariantExportOperations.php deleted file mode 100644 index 62c2d5f6..00000000 --- a/src/AKSRT/Admin/ShopControllerVariantExportOperations.php +++ /dev/null @@ -1,113 +0,0 @@ -get_param('id'); - $product = get_post($product_id); - if (!$product || $product->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->success(['data' => ['variants' => []]]); - } - - $variants = \AKSRT\Shop\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 = method_exists($this, 'get_json_params') - ? $this->get_json_params($request) - : ($request->get_json_params() ?? []); - $variants = $params['variants'] ?? []; - if (!is_array($variants)) { - return $this->error('变体数据格式错误'); - } - - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城系统未启用', 501); - } - - $saved = \AKSRT\Shop\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\Shop')) { - return $this->error('商城系统未启用', 501); - } - - $result = \AKSRT\Shop\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\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\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\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\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\Payment\AfterSale')) { - return $this->success(['data' => \AKSRT\Payment\AfterSale::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\Payment\AfterSale')) { - return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]); - } - return $this->success(['data' => ['refund' => [], 'return' => []]]); - } -} diff --git a/src/AKSRT/Admin/ToolsController.php b/src/AKSRT/Admin/ToolsController.php index 34922511..829e8e72 100644 --- a/src/AKSRT/Admin/ToolsController.php +++ b/src/AKSRT/Admin/ToolsController.php @@ -155,13 +155,6 @@ class ToolsController extends ControllerBase { } return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]); - case 'rebuild_index': - // Rebuild search index / recount stats - if (class_exists('\AKSRT\Forum\Forum') && method_exists(\AKSRT\Forum\Forum::get_instance(), 'recount_stats')) { - \AKSRT\Forum\Forum::get_instance()->recount_stats(); - } - return $this->success(['message' => __('索引重建完成', 'aksrt')]); - default: return $this->error('未知的维护操作: ' . esc_html($action)); } diff --git a/src/AKSRT/Content/PayRest.php b/src/AKSRT/Content/PayRest.php index 8461383f..8ff5e75f 100644 --- a/src/AKSRT/Content/PayRest.php +++ b/src/AKSRT/Content/PayRest.php @@ -152,11 +152,10 @@ trait PayRest { $order = $repo->get_user_order($uid, $order_number); } else { $order = $repo->get($order_number); - // Guest may only poll own guest cash orders (user_id=0): content/topic resource or plate_follow. + // Guest may only poll own guest cash orders (user_id=0): content resource. $ptype = sanitize_key((string) ($order['product_type'] ?? '')); $guest_types = [ \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, ]; if ($order && ((int) ($order['user_id'] ?? -1) !== 0 || !in_array($ptype, $guest_types, true))) { $order = null; diff --git a/src/AKSRT/Core/DBMigrator.php b/src/AKSRT/Core/DBMigrator.php index cda7aff3..b0e4642e 100644 --- a/src/AKSRT/Core/DBMigrator.php +++ b/src/AKSRT/Core/DBMigrator.php @@ -17,12 +17,6 @@ class DBMigrator { const OPT_KEY = 'aksrt_db_schema_version'; - /** - * Historical flag still readable by older code paths. - * Plate CPT is now native when registered; prefer post_type_exists(). - */ - const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated'; - /** * Register hooks. */ diff --git a/src/AKSRT/Core/DBMigratorPart2.php b/src/AKSRT/Core/DBMigratorPart2.php index 3e2227b9..7e00f6db 100644 --- a/src/AKSRT/Core/DBMigratorPart2.php +++ b/src/AKSRT/Core/DBMigratorPart2.php @@ -261,24 +261,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_restriction); - // ---------- 13. 商品变体/SKU ---------- - $sql_variant = "CREATE TABLE {$prefix}aksrt_product_variant ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - product_id BIGINT UNSIGNED NOT NULL, - sku VARCHAR(64) NOT NULL DEFAULT '', - attributes TEXT DEFAULT NULL, - price DECIMAL(10,2) NOT NULL DEFAULT 0, - stock INT NOT NULL DEFAULT 0, - image VARCHAR(512) NOT NULL DEFAULT '', - sort_order INT NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - PRIMARY KEY (id), - KEY idx_product (product_id), - KEY idx_sku (sku), - KEY idx_sort (sort_order) - ) {$charset_collate};"; - dbDelta($sql_variant); - // ── follow ── $sql_follow = "CREATE TABLE {$prefix}aksrt_follow ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -329,64 +311,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_coupon_usage); - // ── after_sale ── - $sql_after_sale = "CREATE TABLE {$prefix}aksrt_after_sale ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL DEFAULT '', - user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - type VARCHAR(20) NOT NULL DEFAULT 'refund', - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'pending', - amount DECIMAL(10,2) NOT NULL DEFAULT 0, - note TEXT DEFAULT NULL, - operator_id BIGINT UNSIGNED DEFAULT NULL, - operator_note TEXT DEFAULT NULL, - refund_request_id VARCHAR(64) DEFAULT NULL, - provider VARCHAR(32) NOT NULL DEFAULT '', - provider_refund_id VARCHAR(128) NOT NULL DEFAULT '', - provider_status VARCHAR(24) NOT NULL DEFAULT 'not_started', - refund_effect_status VARCHAR(24) NOT NULL DEFAULT 'not_started', - refund_error TEXT DEFAULT NULL, - refund_attempts INT UNSIGNED NOT NULL DEFAULT 0, - refund_requested_at DATETIME DEFAULT NULL, - provider_updated_at DATETIME DEFAULT NULL, - refund_succeeded_at DATETIME DEFAULT NULL, - refund_effected_at DATETIME DEFAULT NULL, - order_status_before VARCHAR(20) NOT NULL DEFAULT '', - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - updated_at DATETIME DEFAULT NULL, - resolved_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_refund_request (refund_request_id), - KEY idx_order (order_number), - KEY idx_user (user_id), - KEY idx_status (status), - KEY idx_type (type), - KEY idx_provider_status (provider_status), - KEY idx_created (created_at) - ) {$charset_collate};"; - dbDelta($sql_after_sale); - - // ── order_shipping ── - $sql_shipping = "CREATE TABLE {$prefix}aksrt_order_shipping ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL DEFAULT '', - carrier VARCHAR(60) NOT NULL DEFAULT '', - tracking_no VARCHAR(128) NOT NULL DEFAULT '', - status VARCHAR(20) NOT NULL DEFAULT 'shipped', - note TEXT DEFAULT NULL, - operator_id BIGINT UNSIGNED DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - updated_at DATETIME DEFAULT NULL, - delivered_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_order (order_number), - KEY idx_status (status), - KEY idx_tracking (tracking_no), - KEY idx_created (created_at) - ) {$charset_collate};"; - dbDelta($sql_shipping); - // ── charge_card (zibll-style typed card_pass) ── $sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -461,25 +385,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_order_items); - // ── card_key 卡密表 (§6.2 自动发货) ── - $sql_card_key = "CREATE TABLE {$prefix}aksrt_card_key ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - options_key VARCHAR(255) NOT NULL DEFAULT '', - card_code TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'available', - order_number VARCHAR(32) NOT NULL DEFAULT '', - used_by BIGINT UNSIGNED DEFAULT NULL, - used_at DATETIME DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - KEY idx_product (product_id), - KEY idx_options (options_key(50)), - KEY idx_status (status), - KEY idx_order (order_number) - ) {$charset_collate};"; - dbDelta($sql_card_key); - self::migrate_identity_tables($prefix, $charset_collate); } } diff --git a/src/AKSRT/Core/DBMigratorPart3.php b/src/AKSRT/Core/DBMigratorPart3.php index d3718b6c..45efddfc 100644 --- a/src/AKSRT/Core/DBMigratorPart3.php +++ b/src/AKSRT/Core/DBMigratorPart3.php @@ -31,16 +31,12 @@ trait DBMigratorPart3 { 'report' => 'aksrt_report', 'follow' => 'aksrt_follow', 'coupon' => 'aksrt_coupon', - 'after_sale' => 'aksrt_after_sale', - 'order_shipping' => 'aksrt_order_shipping', 'charge_card' => 'aksrt_charge_card', 'rebate_log' => 'aksrt_rebate_log', - 'coupon_usage' => 'aksrt_coupon_usage', - 'card_key' => 'aksrt_card_key', - 'user_restriction' => 'aksrt_user_restriction', - 'product_variant' => 'aksrt_product_variant', - 'identity_application' => 'aksrt_identity_application', - 'identity_event' => 'aksrt_identity_event', + 'coupon_usage' => 'aksrt_coupon_usage', + 'user_restriction' => 'aksrt_user_restriction', + 'identity_application' => 'aksrt_identity_application', + 'identity_event' => 'aksrt_identity_event', ]; if (!isset($allowed[$name])) { return ''; diff --git a/src/AKSRT/Core/DomainModel.php b/src/AKSRT/Core/DomainModel.php index 0f65b62b..55391b4b 100644 --- a/src/AKSRT/Core/DomainModel.php +++ b/src/AKSRT/Core/DomainModel.php @@ -30,9 +30,6 @@ class DomainModel { public const ORDER_PRODUCT_RECHARGE = 'recharge'; /** Cash purchase of points (zibll order_type=9). */ public const ORDER_PRODUCT_POINTS_BUY = 'points_buy'; - public const ORDER_PRODUCT_SHOP = 'shop_product'; - /** Paid plate follow (zibll order_type=11). product_id = plate post ID. */ - public const ORDER_PRODUCT_PLATE_FOLLOW = 'plate_follow'; /** * Human labels for aksrt_order.product_type (front-end order center). @@ -41,12 +38,10 @@ class DomainModel { */ public static function order_product_type_labels(): array { return [ - self::ORDER_PRODUCT_SHOP => __('商城商品', 'aksrt'), self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'), self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'), self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'), self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'), - self::ORDER_PRODUCT_PLATE_FOLLOW => __('关注版块', 'aksrt'), ]; } diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index 9f59d5ff..c5518ac4 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -635,13 +635,11 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // 交易中心共享壳层(购物车 / 订单 / 收益) - // Virtual rewrite routes (aksrt_orders / aksrt_cart / aksrt_revenue) also count. + // 交易中心共享壳层(订单 / 收益) + // Virtual rewrite routes (aksrt_orders / aksrt_revenue) also count. $is_commerce_center = is_page_template('templates/pages/page-orders.php') - || is_page_template('templates/pages/page-cart.php') || is_page_template('templates/pages/page-revenue.php') || (bool) get_query_var('aksrt_orders') - || (bool) get_query_var('aksrt_cart') || (bool) get_query_var('aksrt_revenue'); if ($is_commerce_center) { $commerce_css = get_template_directory() . '/assets/css/routes/commerce.css'; @@ -735,156 +733,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // Forum pages (home / plate / board tax / tag / topic / compose / plate-cat) - $is_forum_surface = is_page_template('templates/pages/page-forums.php') - || is_page_template('templates/forum/forum-compose.php') - || is_tax('aksrt_forum_board') - || is_tax('aksrt_forum_tag') - || is_tax('aksrt_plate_cat') - || is_singular('aksrt_forum_topic') - || is_singular('aksrt_forum_plate'); - - if ($is_forum_surface) { - wp_enqueue_style( - 'aksrt-forum', - $base_uri . '/assets/css/frontend/forum.css', - ['aksrt-tokens'], - $version - ); - } - - // Product archive pages (category / tag) - if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) { - wp_enqueue_style( - 'aksrt-product-archive', - $base_uri . '/assets/css/frontend/product-archive.css', - ['aksrt-tokens'], - $version - ); - } - - // Product detail page - if (is_page_template('templates/pages/page-product.php')) { - wp_enqueue_style( - 'aksrt-route-product', - $base_uri . '/assets/css/routes/product.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/product.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css') - : $version - ); - wp_enqueue_script( - 'aksrt-product-purchase', - $base_uri . '/assets/js/product-purchase.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/product-purchase.js') - ? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js') - : $version, - true - ); - } - - // Cart page - if (is_page_template('templates/pages/page-cart.php') || (bool) get_query_var('aksrt_cart')) { - wp_enqueue_style( - 'aksrt-route-cart', - $base_uri . '/assets/css/routes/cart.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/cart.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/cart.css') - : $version - ); - wp_enqueue_script( - 'aksrt-cart-checkout', - $base_uri . '/assets/js/cart-checkout.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/cart-checkout.js') - ? (string) filemtime(get_template_directory() . '/assets/js/cart-checkout.js') - : $version, - true - ); - } - - // Product edit page - if (is_page_template('templates/pages/page-product-edit.php')) { - wp_enqueue_style( - 'aksrt-route-product', - $base_uri . '/assets/css/routes/product.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/product.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css') - : $version - ); - wp_enqueue_script( - 'aksrt-product-purchase', - $base_uri . '/assets/js/product-purchase.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/product-purchase.js') - ? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js') - : $version, - true - ); - } - - // Forum edit page - if (is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1') { - $forum_edit_js = get_template_directory() . '/assets/js/forum-edit.js'; - if (file_exists($forum_edit_js)) { - wp_enqueue_script( - 'aksrt-forum-edit', - $base_uri . '/assets/js/forum-edit.js', - ['aksrt-script'], - (string) filemtime($forum_edit_js), - true - ); - } - } - - if ($is_forum_surface) { - $forum_governance_js = get_template_directory() . '/assets/js/forum-governance.js'; - if (file_exists($forum_governance_js)) { - wp_enqueue_script( - 'aksrt-forum-governance', - $base_uri . '/assets/js/forum-governance.js', - ['aksrt-script'], - (string) filemtime($forum_governance_js), - true - ); - wp_localize_script('aksrt-forum-governance', 'aksrtForumGovernance', [ - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'restUrl' => esc_url_raw(rest_url()), - 'restNonce' => wp_create_nonce('wp_rest'), - 'publicNonce' => wp_create_nonce('aksrt_public_nonce'), - 'i18n' => [ - 'empty' => __('内容空空如也', 'aksrt'), - 'loadMore' => __('加载更多', 'aksrt'), - 'loading' => __('加载中…', 'aksrt'), - ], - ]); - } - - // §7.4: enqueue media on compose and edit pages - $is_compose = is_page_template('templates/forum/forum-compose.php'); - $is_edit = is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1'; - if ($is_compose || $is_edit) { - wp_enqueue_media(); - } - } - - // 商品编辑页共用 page-edit.js - if (is_page_template('templates/pages/page-product-edit.php')) { - $page_edit_js = get_template_directory() . '/assets/js/page-edit.js'; - if (file_exists($page_edit_js)) { - wp_enqueue_script( - 'aksrt-page-edit', - $base_uri . '/assets/js/page-edit.js', - ['aksrt-script'], - (string) filemtime($page_edit_js), - true - ); - } - } - // Unified share-data builder — single payload assembler for every share // surface (dropdown, copy, WeChat, poster). Loaded on any singular view. if (is_singular()) { @@ -901,7 +749,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } // 单篇文章 — poster-share.js(海报分享按钮) - if (is_singular('post') || is_singular('aksrt_forum_topic')) { + if (is_singular('post')) { $poster_js = get_template_directory() . '/assets/js/poster-share.js'; if (file_exists($poster_js)) { wp_enqueue_script( @@ -977,20 +825,8 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; $enqueue_module('resource-archive'); } - // 付费论坛主题:多网关收银台(与内容共用 cms-download.js) - $is_pay_forum_topic = is_singular('aksrt_forum_topic') - && sanitize_key((string) get_post_meta(get_queried_object_id(), '_aksrt_visibility', true)) === 'pay'; - - // 版块现金付费关注:多网关收银台 - $is_pay_forum_plate = false; - if (is_singular('aksrt_forum_plate')) { - $plate_qid = get_queried_object_id(); - $is_pay_forum_plate = (int) get_post_meta($plate_qid, '_aksrt_follow_pay', true) === 1 - && sanitize_key((string) (get_post_meta($plate_qid, '_aksrt_follow_pay_modo', true) ?: 'money')) === 'money'; - } - - // 单篇资源 / 付费内容 / 独立下载页 / 付费主题 / 付费关注版块:下载 + 收银台脚本 - if ($is_single_resource || $is_paid_content_single || $is_download_file_page || $is_pay_forum_topic || $is_pay_forum_plate) { + // 单篇资源 / 付费内容 / 独立下载页:下载 + 收银台脚本 + if ($is_single_resource || $is_paid_content_single || $is_download_file_page) { $cms_download_js_path = get_template_directory() . '/assets/js/cms-download.js'; wp_enqueue_script( 'aksrt-cms-download', diff --git a/src/AKSRT/Core/ScriptsPart1c.php b/src/AKSRT/Core/ScriptsPart1c.php index 82c3169c..cef91583 100644 --- a/src/AKSRT/Core/ScriptsPart1c.php +++ b/src/AKSRT/Core/ScriptsPart1c.php @@ -436,9 +436,8 @@ trait ScriptsPart1c { } - // Comment toolbar (smilie/code/image/quick shortcodes) — post form or forum topic reply. - $load_comment_tools = (is_singular() && comments_open()) - || (is_singular('aksrt_forum_topic') || is_singular('forum_topic')); + // Comment toolbar (smilie/code/image/quick shortcodes) — post form. + $load_comment_tools = (is_singular() && comments_open()); if ($load_comment_tools) { $comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js'; wp_enqueue_script( @@ -474,7 +473,7 @@ trait ScriptsPart1c { } } - if (is_singular() || is_page_template('templates/pages/page-forums.php')) { + if (is_singular()) { $page_template_path = get_template_directory() . '/assets/js/page-template.js'; if (file_exists($page_template_path)) { wp_enqueue_script( diff --git a/src/AKSRT/Core/ScriptsPart2.php b/src/AKSRT/Core/ScriptsPart2.php index bd6a72c8..73bb8a03 100644 --- a/src/AKSRT/Core/ScriptsPart2.php +++ b/src/AKSRT/Core/ScriptsPart2.php @@ -222,36 +222,6 @@ trait ScriptsPart2 { } } - } elseif ($type === 'topic' && post_type_exists('aksrt_topic')) { - $posts = get_posts([ - 'post_type' => 'aksrt_topic', - 'post_status' => 'publish', - 's' => $q, - 'posts_per_page' => 6, - 'no_found_rows' => true, - ]); - foreach ($posts as $p) { - $items[] = [ - 'title' => $p->post_title, - 'url' => get_permalink($p), - 'meta' => get_the_date('', $p), - ]; - } - } elseif ($type === 'product' && post_type_exists('aksrt_product')) { - $posts = get_posts([ - 'post_type' => 'aksrt_product', - 'post_status' => 'publish', - 's' => $q, - 'posts_per_page' => 6, - 'no_found_rows' => true, - ]); - foreach ($posts as $p) { - $items[] = [ - 'title' => $p->post_title, - 'url' => get_permalink($p), - 'meta' => get_the_date('', $p), - ]; - } } elseif ($type === 'tag') { $terms = get_terms([ 'taxonomy' => 'post_tag', @@ -294,7 +264,6 @@ trait ScriptsPart2 { */ private function resolve_frontend_routes(): array { return [ - 'cart' => $this->resolve_template_page_url('templates/pages/page-cart.php', '/cart/'), 'orders' => $this->resolve_template_page_url('templates/pages/page-orders.php', '/orders/'), 'resources' => home_url('/resources/'), ]; diff --git a/src/AKSRT/Core/Setup.php b/src/AKSRT/Core/Setup.php index 8e87c415..729d9542 100644 --- a/src/AKSRT/Core/Setup.php +++ b/src/AKSRT/Core/Setup.php @@ -31,20 +31,6 @@ class Setup { add_action('init', [$this, 'register_nav_menus'], 0); add_action('init', [$this, 'register_post_types'], 1); add_action('init', [$this, 'register_meta_fields'], 5); - add_action('init', [$this, 'register_shop_rewrite_rules'], 20); - add_filter('query_vars', [$this, 'register_shop_query_vars']); - add_action('template_redirect', [$this, 'shop_template_redirect']); - // Shop fulfillment hook (§6.5 R68) - if (class_exists('\AKSRT\Shop\Shop')) { - add_action('aksrt_order_paid', [\AKSRT\Shop\Shop::get_instance(), 'handle_order_paid'], 10, 2); - } - // Auto-confirm receipt cron (§6.5 R68) - if (!wp_next_scheduled('aksrt_auto_confirm_receipt')) { - wp_schedule_event(time(), 'hourly', 'aksrt_auto_confirm_receipt'); - } - if (class_exists('\AKSRT\Shop\Shop')) { - add_action('aksrt_auto_confirm_receipt', [\AKSRT\Shop\Shop::get_instance(), 'run_auto_confirm_cron']); - } // 文章浏览量统计(单次访问) add_action('wp', 'aksrt_set_post_views'); // 隐藏默认WordPress管理菜单 @@ -110,97 +96,6 @@ class Setup { * 通过 _aksrt_is_resource meta 标记是否为资源。 */ public function register_post_types(): void { - // Shop rewrite settings (safe fallback if settings not yet loaded) - $shop_rewrite = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_rewrite = $shop['rewrite'] ?? []; - } - $product_slug = sanitize_key((string) ($shop_rewrite['product_slug'] ?? 'product')); - $suffix_html = !empty($shop_rewrite['suffix_html']); - - // Shop admin visibility: align with zibll native sidebar when shop is enabled. - $shop_enabled = true; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop_settings = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_enabled = !empty($shop_settings['enabled']); - } - // Always register UI in admin so settings/tools remain reachable; hide menu when disabled. - $shop_show_ui = true; - $shop_show_menu = is_admin() ? $shop_enabled : false; - - // 商品 CPT — native WP sidebar (zibll: shop_product @ pos 35) - register_post_type('aksrt_product', [ - 'labels' => [ - 'name' => __('商城', 'aksrt'), - 'singular_name' => __('商品', 'aksrt'), - 'add_new' => __('新增商品', 'aksrt'), - 'add_new_item' => __('新增商品', 'aksrt'), - 'edit_item' => __('编辑商品', 'aksrt'), - 'new_item' => __('新商品', 'aksrt'), - 'view_item' => __('查看商品', 'aksrt'), - 'search_items' => __('搜索商品', 'aksrt'), - 'not_found' => __('未找到商品', 'aksrt'), - 'not_found_in_trash' => __('回收站中无商品', 'aksrt'), - 'all_items' => __('所有商品', 'aksrt'), - 'menu_name' => __('商城', 'aksrt'), - ], - 'public' => true, - 'publicly_queryable' => true, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'has_archive' => true, - 'hierarchical' => false, - 'menu_position' => 35, - 'menu_icon' => 'dashicons-store', - 'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'], - 'rewrite' => ['slug' => $product_slug, 'with_front' => false], - 'capability_type' => 'post', - 'map_meta_cap' => true, - ]); - - // 商品分类 taxonomy(独立于文章 category) - register_taxonomy('aksrt_product_cat', 'aksrt_product', [ - 'labels' => [ - 'name' => __('商品分类', 'aksrt'), - 'singular_name' => __('商品分类', 'aksrt'), - 'search_items' => __('搜索商品分类', 'aksrt'), - 'all_items' => __('所有商品分类', 'aksrt'), - 'parent_item' => __('父级分类', 'aksrt'), - 'edit_item' => __('编辑分类', 'aksrt'), - 'add_new_item' => __('新增分类', 'aksrt'), - 'menu_name' => __('商品分类', 'aksrt'), - ], - 'public' => true, - 'hierarchical' => true, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'product-cat', 'with_front' => false], - ]); - - // 商品标签 taxonomy(独立于文章 post_tag) - register_taxonomy('aksrt_product_tag', 'aksrt_product', [ - 'labels' => [ - 'name' => __('商品标签', 'aksrt'), - 'singular_name' => __('商品标签', 'aksrt'), - 'search_items' => __('搜索商品标签', 'aksrt'), - 'all_items' => __('所有商品标签', 'aksrt'), - 'edit_item' => __('编辑标签', 'aksrt'), - 'add_new_item' => __('新增标签', 'aksrt'), - 'menu_name' => __('特色标签', 'aksrt'), - ], - 'public' => true, - 'hierarchical' => false, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'product-tag', 'with_front' => false], - ]); - // 项目展示 register_post_type('project', [ 'labels' => [ diff --git a/src/AKSRT/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index a99dfc97..350d8f83 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -382,53 +382,6 @@ trait SetupPart1 { 'after_title' => $title_after, ]); - // ── Forum / BBS widget areas (zibll bbs_* parity lite) ── - $forum_widgets = [ - 'bbs_home_sidebar' => [__('论坛首页 · 侧栏', 'aksrt'), __('论坛首页侧栏区域。', 'aksrt')], - 'bbs_home_top_fluid' => [__('论坛首页 · 顶部全宽', 'aksrt'), __('论坛首页顶部全宽。', 'aksrt')], - 'bbs_plate_sidebar' => [__('论坛版块 · 侧栏', 'aksrt'), __('版块页侧栏。', 'aksrt')], - 'bbs_plate_top_fluid' => [__('论坛版块 · 顶部全宽', 'aksrt'), __('版块页顶部全宽。', 'aksrt')], - 'bbs_posts_sidebar' => [__('论坛帖子 · 侧栏', 'aksrt'), __('帖子页侧栏挂件区。', 'aksrt')], - 'bbs_posts_top_fluid' => [__('论坛帖子 · 顶部全宽', 'aksrt'), __('帖子页顶部全宽。', 'aksrt')], - 'bbs_new_posts_sidebar_top' => [__('论坛发帖 · 侧栏顶部', 'aksrt'), __('发帖页侧栏顶部。', 'aksrt')], - 'bbs_new_posts_sidebar_bottom' => [__('论坛发帖 · 侧栏底部', 'aksrt'), __('发帖页侧栏底部。', 'aksrt')], - ]; - foreach ($forum_widgets as $sid => $meta) { - register_sidebar([ - 'id' => $sid, - 'name' => $meta[0], - 'description' => $meta[1], - 'before_widget' => $card_wrapper_before, - 'after_widget' => '', - 'before_title' => $title_before, - 'after_title' => $title_after, - ]); - } - - // ── Shop widget areas (§6.9) ── - $shop_widgets = [ - 'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'], - 'shop_home_sidebar' => ['商城首页 · 侧栏', '商城首页侧栏区域。'], - 'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'], - 'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'], - 'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'], - 'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'], - 'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'], - 'shop_order_top_fluid' => ['订单页 · 顶部通栏', '订单中心顶部全宽。'], - 'shop_order_bottom_fluid' => ['订单页 · 底部通栏', '订单中心底部全宽。'], - ]; - foreach ($shop_widgets as $sid => $meta) { - register_sidebar([ - 'id' => $sid, - 'name' => $meta[0], - 'description' => $meta[1], - 'before_widget' => $card_wrapper_before, - 'after_widget' => '', - 'before_title' => $title_before, - 'after_title' => $title_after, - ]); - } - // ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ── // Pages that opt in via meta `widgets_register=1` + `widgets_register_container` // (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and diff --git a/src/AKSRT/Core/SetupPart2.php b/src/AKSRT/Core/SetupPart2.php index ce2d6191..f53d6d4c 100644 --- a/src/AKSRT/Core/SetupPart2.php +++ b/src/AKSRT/Core/SetupPart2.php @@ -23,100 +23,5 @@ trait SetupPart2 { // 移除仪表盘中的不必要组件 remove_meta_box('dashboard_primary', 'dashboard', 'normal'); remove_meta_box('dashboard_quick_press', 'dashboard', 'side'); - - // Separators around shop (35) and forum (40) — match zibll admin_menu_separator. - global $menu; - if (!is_array($menu)) { - return; - } - if (!isset($menu[34])) { - $menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator']; - } - if (!isset($menu[39])) { - $menu[39] = ['', 'read', 'separator-aksrt-forum', '', 'wp-menu-separator']; - } - } - - /** - * Register shop rewrite rules for: - * - /shop/{ID}.html — product detail (ID-based, .html suffix) - * - /shop/{ID}.html/comment-{page} — product comment pagination - * - /{cart_slug}/ — cart page (fixed alias) - * - * Runs at init priority 20 (after CPT registration at priority 1). - */ - public function register_shop_rewrite_rules(): void { - if (!class_exists('\AKSRT\Settings\Settings')) { - return; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - if (empty($shop['enabled'])) { - return; - } - - $rw = $shop['rewrite'] ?? []; - $product_slug = sanitize_key((string) ($rw['product_slug'] ?? 'product')); - $cart_slug = sanitize_key((string) ($rw['cart_slug'] ?? 'cart')); - $suffix_html = !empty($rw['suffix_html']); - - // Product ID-based .html routes: /shop/{ID}.html - if ($suffix_html) { - add_rewrite_rule( - '^shop/(\d+)\.html/comment-page-([0-9]{1,})/?$', - 'index.php?post_type=aksrt_product&p=$matches[1]&cpage=$matches[2]', - 'top' - ); - add_rewrite_rule( - '^shop/(\d+)\.html/?$', - 'index.php?post_type=aksrt_product&p=$matches[1]', - 'top' - ); - // Slug-based .html: /{product_slug}/{slug}.html - add_rewrite_rule( - '^' . preg_quote($product_slug) . '/([^/]+)\.html/comment-page-([0-9]{1,})/?$', - 'index.php?post_type=aksrt_product&aksrt_product=$matches[1]&cpage=$matches[3]', - 'top' - ); - add_rewrite_rule( - '^' . preg_quote($product_slug) . '/([^/]+)\.html/?$', - 'index.php?post_type=aksrt_product&aksrt_product=$matches[1]', - 'top' - ); - } - - // Cart page: /{cart_slug}/ - add_rewrite_rule( - '^' . preg_quote($cart_slug) . '/?$', - 'index.php?pagename=' . $cart_slug, - 'top' - ); - } - - /** - * Register shop-specific query vars. - */ - public function register_shop_query_vars(array $vars): array { - $vars[] = 'product_id'; - $vars[] = 'shop_action'; - return $vars; - } - - /** - * Template redirect for shop pages. - * - * - Single aksrt_product: serve page-product.php template - * - Cart page: serve page-cart.php template - */ - public function shop_template_redirect(): void { - // Single product: if the main query resolves to aksrt_product, use our template - if (is_singular('aksrt_product')) { - $tpl = AKSRT_DIR . '/templates/pages/page-product.php'; - if (file_exists($tpl)) { - // Set product_id query var for template consumption - set_query_var('product_id', get_queried_object_id()); - include $tpl; - exit; - } - } } } diff --git a/src/AKSRT/Core/Shortcodes.php b/src/AKSRT/Core/Shortcodes.php index 65dde2e8..a92bd57b 100644 --- a/src/AKSRT/Core/Shortcodes.php +++ b/src/AKSRT/Core/Shortcodes.php @@ -178,29 +178,6 @@ class Shortcodes { return true; } - // Forum topics: paid/credits unlock list or orders. - if ($post->post_type === 'aksrt_forum_topic' && class_exists('\AKSRT\Forum\Forum')) { - $forum = \AKSRT\Forum\Forum::get_instance(); - $visibility = sanitize_key((string) get_post_meta($pid, '_aksrt_visibility', true)); - if ($visibility === 'public' || $visibility === '') { - return true; - } - if (method_exists($forum, 'has_user_unlocked_topic') && $forum->has_user_unlocked_topic($uid, $pid)) { - return true; - } - if (method_exists($forum, 'has_user_paid_topic') && $forum->has_user_paid_topic($uid, $pid)) { - return true; - } - // Free VIP price counts as paid for cash topics. - if ($visibility === 'pay' && method_exists($forum, 'resolve_topic_pay_price')) { - $price = $forum->resolve_topic_pay_price($pid, $uid); - if (!empty($price['is_free'])) { - return true; - } - } - return false; - } - if (class_exists('\AKSRT\Content\Visibility')) { return \AKSRT\Content\Visibility::can_view_paid_content($pid); } diff --git a/src/AKSRT/Core/Templates.php b/src/AKSRT/Core/Templates.php index afe37e70..510bd6b6 100644 --- a/src/AKSRT/Core/Templates.php +++ b/src/AKSRT/Core/Templates.php @@ -24,7 +24,6 @@ class Templates { public function init(): void { add_filter('theme_page_templates', [$this, 'register_page_templates']); add_filter('template_include', [$this, 'load_page_template']); - add_filter('template_include', [$this, 'load_taxonomy_template']); add_filter('page_template_hierarchy', [$this, 'template_hierarchy']); add_filter('aksrt_page_template_hierarchy', [$this, 'template_hierarchy']); @@ -51,12 +50,7 @@ class Templates { 'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'), 'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'), 'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'), - 'templates/pages/page-cart.php' => __('AKSRT - 购物车', 'aksrt'), 'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'), - 'templates/pages/page-product.php' => __('AKSRT - 商品详情', 'aksrt'), - 'templates/pages/page-product-edit.php' => __('AKSRT - 商品编辑', 'aksrt'), - 'templates/pages/page-forums.php' => __('AKSRT - 论坛', 'aksrt'), - 'templates/forum/forum-compose.php' => __('AKSRT - 发帖', 'aksrt'), ]; } @@ -104,23 +98,9 @@ class Templates { } /** - * Load dedicated product taxonomy templates. + * Load dedicated taxonomy templates. */ public function load_taxonomy_template(string $template): string { - if (is_tax('aksrt_product_cat')) { - $cat_template = AKSRT_DIR . '/templates/shop/product-cat.php'; - if (file_exists($cat_template)) { - return $cat_template; - } - } - - if (is_tax('aksrt_product_tag')) { - $tag_template = AKSRT_DIR . '/templates/shop/product-tag.php'; - if (file_exists($tag_template)) { - return $tag_template; - } - } - return $template; } diff --git a/src/AKSRT/Core/WidgetAjax.php b/src/AKSRT/Core/WidgetAjax.php index 3c013bb7..ce64839f 100644 --- a/src/AKSRT/Core/WidgetAjax.php +++ b/src/AKSRT/Core/WidgetAjax.php @@ -35,9 +35,6 @@ class WidgetAjax { '/widgets/oneline-posts' => 'rest_oneline_posts', '/widgets/recent-comments' => 'rest_recent_comments', '/widgets/user-lists' => 'rest_user_lists', - '/widgets/forum-topics' => 'rest_forum_topics', - '/widgets/forum-terms' => 'rest_forum_terms', - '/widgets/forum-boards' => 'rest_forum_boards', '/widgets/tag-cloud' => 'rest_tag_cloud', '/widgets/categories' => 'rest_categories', '/widgets/term-cards' => 'rest_term_cards', @@ -264,42 +261,6 @@ class WidgetAjax { return new \WP_REST_Response(['success' => true, 'html' => $html], 200); } - public function rest_forum_topics(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) { - return $this->fail(); - } - $style = sanitize_key((string) ($request->get_param('style') ?? 'mini')); - if (!in_array($style, ['mini', 'rank'], true)) { - $style = 'mini'; - } - $instance = [ - 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))), - 'period' => sanitize_key((string) ($request->get_param('period') ?? 'week')), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'views')), - 'style' => $style, - 'plate' => max(0, (int) ($request->get_param('plate') ?? $request->get_param('plate_id') ?? 0)), - 'paginate' => in_array(sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')) : 'ajax', - ]; - $page = max(1, (int) ($request->get_param('page') ?? 1)); - return $this->payload(\AKSRT\Widgets\WidgetForumHotTopics::query_payload($instance, $page), $page); - } - - public function rest_forum_terms(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumTopicTerms')) { - return $this->fail(); - } - $instance = [ - 'number' => max(1, min(40, (int) ($request->get_param('number') ?? 12))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')), - 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), - 'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')), - ]; - $html = \AKSRT\Widgets\WidgetForumTopicTerms::render_html($instance); - return new \WP_REST_Response(['success' => true, 'html' => $html], 200); - } - - public function rest_category_block(\WP_REST_Request $request) { if (!class_exists('\AKSRT\Widgets\CategoryBlockWidget')) { return $this->fail(); @@ -337,20 +298,6 @@ class WidgetAjax { return new \WP_REST_Response(['success' => true, 'html' => $html], 200); } - public function rest_forum_boards(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { - return $this->fail(); - } - $instance = [ - 'count' => max(1, min(50, (int) ($request->get_param('count') ?? 30))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'list')), - 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), - ]; - $html = \AKSRT\Widgets\WidgetForumBoardList::render_html($instance); - return new \WP_REST_Response(['success' => true, 'html' => $html], 200); - } - public function rest_tag_cloud(\WP_REST_Request $request) { if (!class_exists('\AKSRT\Widgets\TagCloudWidget')) { return $this->fail(); diff --git a/src/AKSRT/Credits/Earn.php b/src/AKSRT/Credits/Earn.php index 61115c02..257e1486 100644 --- a/src/AKSRT/Credits/Earn.php +++ b/src/AKSRT/Credits/Earn.php @@ -32,10 +32,6 @@ class Earn { add_action('aksrt_post_favorited', [$this, 'on_post_favorited'], 10, 3); add_action('aksrt_comment_liked', [$this, 'on_comment_liked'], 10, 3); add_action('aksrt_user_followed', [$this, 'on_user_followed'], 10, 2); - - add_action('aksrt_forum_topic_created', [$this, 'on_forum_topic_created'], 10, 2); - add_action('comment_post', [$this, 'on_forum_reply_posted'], 20, 3); - add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 10, 2); } public function on_user_register(int $user_id): void { @@ -124,11 +120,6 @@ class Earn { if (get_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', true)) { return; } - // Skip forum topic replies (handled separately). - $post = get_post((int) $comment->comment_post_ID); - if ($post && $post->post_type === 'aksrt_forum_topic') { - return; - } $cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_credit_settings() : []; $reward = is_array($cfg['comment_reward'] ?? null) ? $cfg['comment_reward'] : []; $min_length = (int) ($reward['min_length'] ?? 0); @@ -200,48 +191,4 @@ class Earn { } PointsFree::grant($following_id, 'followed', 0, ['type' => 'user', 'id' => $follower_id]); } - - public function on_forum_topic_created(int $user_id, int $post_id): void { - if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) { - return; - } - PointsFree::grant($user_id, 'bbs_posts_new', 0, ['type' => 'post', 'id' => $post_id]); - } - - public function on_forum_reply_posted(int $comment_id, $approved, array $commentdata = []): void { - unset($commentdata); - if ((int) $approved !== 1 && $approved !== '1' && $approved !== true) { - // Still load comment and check. - } - $comment = get_comment($comment_id); - if (!$comment || (int) $comment->comment_approved !== 1) { - return; - } - $post = get_post((int) $comment->comment_post_ID); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $user_id = (int) $comment->user_id; - if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) { - return; - } - if (get_comment_meta($comment_id, 'aksrt_credit_forum_reply', true)) { - return; - } - $granted = PointsFree::grant($user_id, 'bbs_reply', 0, ['type' => 'comment', 'id' => $comment_id]); - if (!is_wp_error($granted)) { - update_comment_meta($comment_id, 'aksrt_credit_forum_reply', 1); - } - } - - /** - * Essence reward is owned by \AKSRT\Forum\Forum::reward_essence() (free-day-cap). - * Keep hook for external integrations only — do not double-grant. - * - * @param int $topic_id - * @param int $author_or_operator Author id from forum (2nd arg) or operator. - */ - public function on_forum_topic_featured(int $topic_id, int $author_or_operator = 0): void { - unset($topic_id, $author_or_operator); - } } diff --git a/src/AKSRT/Credits/PointsFree.php b/src/AKSRT/Credits/PointsFree.php index a36df371..c38ab0ee 100644 --- a/src/AKSRT/Credits/PointsFree.php +++ b/src/AKSRT/Credits/PointsFree.php @@ -30,13 +30,6 @@ class PointsFree { 'post_favorite' => [__('文章被收藏', 'aksrt'), __('你的文章被收藏', 'aksrt'), 1], 'comment_new' => [__('发表评论', 'aksrt'), __('发表并通过审核的评论', 'aksrt'), 1], 'comment_like' => [__('评论被赞', 'aksrt'), __('你的评论被点赞', 'aksrt'), 1], - 'bbs_posts_new' => [__('论坛发帖', 'aksrt'), __('发布论坛主题', 'aksrt'), 5], - 'bbs_reply' => [__('论坛回复', 'aksrt'), __('回复论坛主题', 'aksrt'), 2], - 'bbs_essence' => [__('帖子加精', 'aksrt'), __('主题被设为精华', 'aksrt'), 10], - 'bbs_adopt' => [__('回复被采纳', 'aksrt'), __('问答帖回复被采纳', 'aksrt'), 20], - 'bbs_posts_hot' => [__('热帖奖励', 'aksrt'), __('主题成为热帖', 'aksrt'), 5], - 'bbs_comment_hot'=> [__('神评奖励', 'aksrt'), __('回复成为神评', 'aksrt'), 5], - 'bbs_score_extra'=> [__('帖子加分', 'aksrt'), __('主题被加分时奖励', 'aksrt'), 3], ]; } @@ -194,8 +187,8 @@ class PointsFree { $task_dates = is_array($raw) ? $raw : []; } - $passive = ['post_like', 'post_favorite', 'followed', 'comment_like', 'bbs_essence', 'bbs_adopt', 'bbs_posts_hot', 'bbs_comment_hot', 'bbs_score_extra']; - $active_once = ['post_new', 'comment_new', 'bbs_posts_new', 'bbs_reply']; + $passive = ['post_like', 'post_favorite', 'followed', 'comment_like']; + $active_once = ['post_new', 'comment_new']; foreach (self::catalog() as $key => $meta) { $pts = (int) ($cfg['tasks'][$key] ?? 0); @@ -333,48 +326,6 @@ class PointsFree { return 0; } return max(0, (int) ($cr['amount'] ?? $default)); - case 'bbs_posts_new': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['post_new'] ?? $f['credits']['topic_new'] ?? $all['bbs_credits_post_new'] ?? $default)); - } - return max(0, (int) ($all['bbs_credits_post_new'] ?? $default)); - case 'bbs_reply': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['comment_new'] ?? $default)); - } - return $default; - case 'bbs_essence': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['essence'] ?? $default)); - } - return $default; - case 'bbs_adopt': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['adopted'] ?? $default)); - } - return $default; - case 'bbs_posts_hot': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['posts_hot'] ?? $default)); - } - return $default; - case 'bbs_comment_hot': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['comment_hot'] ?? $f['credits']['comment_like'] ?? $default)); - } - return $default; - case 'bbs_score_extra': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['score_extra'] ?? $default)); - } - return $default; default: return $default; } diff --git a/src/AKSRT/Forum/BoardService.php b/src/AKSRT/Forum/BoardService.php deleted file mode 100644 index e6b7c9b0..00000000 --- a/src/AKSRT/Forum/BoardService.php +++ /dev/null @@ -1,396 +0,0 @@ -forum = $forum; - } - - /** - * Get board list (plate CPT). - * - * 读取 aksrt_forum_plate CPT,支持层级查询。 - * Fallback:若 plate CPT 不存在或迁移未完成,降级读取 taxonomy。 - */ - public function get_boards(array $args = []): array { - // 优先使用 plate CPT - if ($this->forum->is_plate_cpt_active()) { - return $this->get_boards_from_cpt($args); - } - - // Fallback: 读取旧 taxonomy(向后兼容) - return $this->get_boards_from_taxonomy(); - } - - /** - * 从 plate CPT 读取板块列表(新实现)。 - */ - private function get_boards_from_cpt(array $args = []): array { - $defaults = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'hierarchical' => true, - ]; - - $query_args = wp_parse_args($args, $defaults); - $plates = get_posts($query_args); - - if (empty($plates)) { - return []; - } - - $boards = []; - foreach ($plates as $plate) { - $plate_id = (int) $plate->ID; - $moderators = $this->forum->get_plate_moderators($plate_id); - $applications = $this->forum->get_plate_moderator_applications($plate_id); - - $boards[] = [ - 'id' => $plate_id, - 'name' => (string) $plate->post_title, - 'description' => (string) $plate->post_excerpt, - 'content' => (string) $plate->post_content, - 'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true), - 'sort_order' => (int) $plate->menu_order, - 'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true), - 'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true), - 'views' => (int) get_post_meta($plate_id, '_aksrt_views', true), - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true), - 'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true), - 'moderator_count' => count($moderators), - 'pending_application_count' => count(array_filter($applications, static function ($item): bool { - return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING; - })), - 'url' => (string) get_permalink($plate), - 'author_id' => (int) $plate->post_author, - 'parent_id' => (int) $plate->post_parent, - 'created_at' => (string) $plate->post_date, - 'thumbnail_url' => (string) get_the_post_thumbnail_url($plate, 'thumbnail'), - ]; - } - - return $this->build_hierarchical_boards($boards); - } - - /** - * 从 taxonomy 读取板块(向后兼容)。 - */ - private function get_boards_from_taxonomy(): array { - $terms = get_terms([ - 'taxonomy' => 'aksrt_forum_board', - 'hide_empty' => false, - 'orderby' => 'meta_value_num', - 'meta_key' => '_aksrt_sort_order', - 'order' => 'ASC', - ]); - - if (is_wp_error($terms) || empty($terms)) { - return []; - } - - $boards = []; - foreach ($terms as $term) { - $board_id = (int) $term->term_id; - $moderators = $this->forum->get_board_moderators($board_id); - $applications = $this->forum->get_board_moderator_applications($board_id); - $board_link = get_term_link($term); - - $boards[] = [ - 'id' => $board_id, - 'name' => (string) $term->name, - 'description' => (string) $term->description, - 'content' => '', - 'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true), - 'sort_order' => (int) get_term_meta($board_id, '_aksrt_sort_order', true), - 'post_count' => (int) $term->count, - 'reply_count' => 0, - 'today_reply_count' => 0, - 'views' => 0, - 'follow_count' => 0, - 'last_reply' => '', - 'last_post' => '', - 'moderator_count' => count($moderators), - 'pending_application_count' => count(array_filter($applications, static function ($item): bool { - return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING; - })), - 'url' => !is_wp_error($board_link) ? (string) $board_link : '', - 'author_id' => 0, - 'parent_id' => (int) $term->parent, - 'created_at' => '', - 'thumbnail_url' => '', - ]; - } - - return $boards; - } - - /** - * 构建层级板块树(子板块嵌套)。 - */ - private function build_hierarchical_boards(array $boards): array { - $by_id = []; - foreach ($boards as $board) { - $by_id[$board['id']] = $board; - $by_id[$board['id']]['children'] = []; - } - - $roots = []; - foreach ($boards as $board) { - if ($board['parent_id'] > 0 && isset($by_id[$board['parent_id']])) { - $by_id[$board['parent_id']]['children'][] = &$by_id[$board['id']]; - } else { - $roots[] = &$by_id[$board['id']]; - } - } - - return $roots; - } - - /** - * Create board (统一入口,自动选择 CPT/taxonomy). - */ - public function create_board(array $data): int { - if ($this->forum->is_plate_cpt_active()) { - return $this->create_plate($data); - } - - // Fallback: taxonomy - $name = sanitize_text_field((string) ($data['name'] ?? '')); - if ($name === '') { - return 0; - } - - $result = wp_insert_term($name, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($data['description'] ?? '')), - ]); - - if (is_wp_error($result)) { - return 0; - } - - $term_id = (int) $result['term_id']; - $icon = sanitize_text_field((string) ($data['icon'] ?? '')); - $sort_order = (int) ($data['sort_order'] ?? $term_id); - - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - - update_term_meta($term_id, '_aksrt_sort_order', $sort_order); - - return $term_id; - } - - /** - * Update board (统一入口,自动选择 CPT/taxonomy). - */ - public function update_board(int $board_id, array $data): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->update_plate($board_id, $data); - } - - // Fallback: taxonomy - $term = get_term($board_id, 'aksrt_forum_board'); - if (!$term || is_wp_error($term)) { - return false; - } - - $update = ['taxonomy' => 'aksrt_forum_board']; - - if (isset($data['name'])) { - $name = sanitize_text_field((string) $data['name']); - if ($name !== '') { - $update['name'] = $name; - } - } - - if (isset($data['description'])) { - $update['description'] = sanitize_textarea_field((string) $data['description']); - } - - $result = wp_update_term($board_id, 'aksrt_forum_board', $update); - if (is_wp_error($result)) { - return false; - } - - if (isset($data['icon'])) { - update_term_meta($board_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - - if (isset($data['sort_order'])) { - update_term_meta($board_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - - return true; - } - - /** - * Delete board (统一入口,自动选择 CPT/taxonomy). - */ - public function delete_board(int $board_id): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->delete_plate($board_id); - } - - // Fallback: taxonomy - $term = get_term($board_id, 'aksrt_forum_board'); - if (!$term || is_wp_error($term)) { - return false; - } - - $result = wp_delete_term($board_id, 'aksrt_forum_board'); - return !is_wp_error($result) && !empty($result); - } - - /** - * Reorder boards (统一入口). - */ - public function reorder_boards(array $order_map): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->reorder_plates($order_map); - } - - foreach ($order_map as $term_id => $sort_order) { - $term_id = (int) $term_id; - if ($term_id <= 0) { - continue; - } - - update_term_meta($term_id, '_aksrt_sort_order', (int) $sort_order); - } - - return true; - } - - // ================================================================ - // §7.0 Plate CPT methods —板块 CPT 化 CRUD - // ================================================================ - - /** - * Get all plates (板块 CPT posts). - * - * @return array> - */ - public function get_plates(array $args = []): array { - $query_args = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => (int) ($args['limit'] ?? 100), - 'orderby' => 'menu_order', - 'order' => 'ASC', - ]; - - if (!empty($args['parent'])) { - $query_args['post_parent'] = (int) $args['parent']; - } - - if (!empty($args['plate_cat'])) { - $query_args['tax_query'] = [[ - 'taxonomy' => 'aksrt_plate_cat', - 'field' => 'term_id', - 'terms' => (int) $args['plate_cat'], - ]]; - } - - $posts = get_posts($query_args); - $plates = []; - - foreach ($posts as $post) { - $plates[] = $this->build_plate_payload($post); - } - - return $plates; - } - - /** - * Get single plate by ID. - */ - public function get_plate(int $plate_id): ?array { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return null; - } - - return $this->build_plate_payload($post); - } - - /** - * Build plate data payload from \WP_Post. - */ - public function build_plate_payload(\WP_Post $post): array { - $plate_id = (int) $post->ID; - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - $moderators = []; - } - - $cover_url = (string) get_the_post_thumbnail_url($plate_id, 'medium'); - if ($cover_url === '') { - $config = $this->forum->get_config(); - $default_thumbs = $config['other']['bbs_term_thumb'] ?? []; - if (!empty($default_thumbs) && is_array($default_thumbs)) { - $cover_url = $default_thumbs[array_rand($default_thumbs)]; - } - } - - $plate_cats = wp_get_object_terms($plate_id, 'aksrt_plate_cat', ['fields' => 'all']); - $plate_cat = (!is_wp_error($plate_cats) && !empty($plate_cats)) ? [ - 'id' => (int) $plate_cats[0]->term_id, - 'name' => (string) $plate_cats[0]->name, - 'slug' => (string) $plate_cats[0]->slug, - ] : null; - - return [ - 'id' => $plate_id, - 'name' => (string) $post->post_title, - 'description' => (string) $post->post_excerpt, - 'content' => (string) $post->post_content, - 'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true), - 'cover_url' => $cover_url, - 'sort_order' => (int) $post->menu_order, - 'parent_id' => (int) $post->post_parent, - 'author_id' => (int) $post->post_author, - 'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true), - 'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true), - 'views' => (int) get_post_meta($plate_id, '_aksrt_views', true), - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true), - 'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true), - 'moderator_count' => count($moderators), - 'moderators' => $moderators, - 'url' => (string) get_permalink($plate_id), - 'created_at' => (string) $post->post_date, - 'allow_view' => (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'), - 'add_limit' => (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'), - 'is_hot' => $this->forum->is_hot_plate($plate_id), - 'plate_cat' => $plate_cat, - // Alias fields for backward compat with get_board_view consumer - 'topic_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'thumbnail_url' => $cover_url, - ]; - } - - /** - * Create plate (CPT post). - */ -} diff --git a/src/AKSRT/Forum/BoardServicePart1.php b/src/AKSRT/Forum/BoardServicePart1.php deleted file mode 100644 index a482bd8a..00000000 --- a/src/AKSRT/Forum/BoardServicePart1.php +++ /dev/null @@ -1,425 +0,0 @@ - 'aksrt_forum_plate', - 'post_title' => $title, - 'post_content' => wp_kses_post((string) ($data['content'] ?? '')), - 'post_excerpt' => sanitize_textarea_field((string) ($data['description'] ?? '')), - 'post_status' => 'publish', - 'post_parent' => (int) ($data['parent_id'] ?? 0), - 'post_author' => (int) ($data['author_id'] ?? get_current_user_id()), - 'menu_order' => (int) ($data['sort_order'] ?? 0), - ], true); - - if (is_wp_error($plate_id)) { - return 0; - } - - $icon = sanitize_text_field((string) ($data['icon'] ?? '')); - if ($icon !== '') { - update_post_meta($plate_id, '_aksrt_icon', $icon); - } - - update_post_meta($plate_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0)); - update_post_meta($plate_id, '_aksrt_posts_count', 0); - update_post_meta($plate_id, '_aksrt_reply_count', 0); - update_post_meta($plate_id, '_aksrt_follow_count', 0); - update_post_meta($plate_id, '_aksrt_plate_views', 0); - - if (!empty($data['plate_cat'])) { - wp_set_object_terms($plate_id, [(int) $data['plate_cat']], 'aksrt_plate_cat'); - } - - // Also create a corresponding taxonomy term for backward compat - $term_result = wp_insert_term($title, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($data['description'] ?? '')), - ]); - if (!is_wp_error($term_result)) { - $term_id = (int) $term_result['term_id']; - update_term_meta($term_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0)); - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - update_post_meta($plate_id, '_aksrt_migrated_from_term', $term_id); - } - - return $plate_id; - } - - /** - * Update plate. - */ - public function update_plate(int $plate_id, array $data): bool { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return false; - } - - $update = ['ID' => $plate_id]; - - if (isset($data['name'])) { - $name = sanitize_text_field((string) $data['name']); - if ($name !== '') { - $update['post_title'] = $name; - } - } - - if (isset($data['description'])) { - $update['post_excerpt'] = sanitize_textarea_field((string) $data['description']); - } - - if (isset($data['content'])) { - $update['post_content'] = wp_kses_post((string) $data['content']); - } - - if (isset($data['parent_id'])) { - $update['post_parent'] = (int) $data['parent_id']; - } - - if (isset($data['sort_order'])) { - $update['menu_order'] = (int) $data['sort_order']; - update_post_meta($plate_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - - if (isset($data['author_id'])) { - $update['post_author'] = (int) $data['author_id']; - } - - $result = wp_update_post($update, true); - if (is_wp_error($result)) { - return false; - } - - if (isset($data['icon'])) { - update_post_meta($plate_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - - if (isset($data['allow_view'])) { - $allowed_views = ['public', 'login', 'role', 'follow']; - $view = in_array($data['allow_view'], $allowed_views, true) ? $data['allow_view'] : 'public'; - update_post_meta($plate_id, '_aksrt_allow_view', $view); - } - - // Sync to legacy taxonomy term if exists - $term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true); - if ($term_id > 0) { - $term_update = []; - if (isset($update['post_title'])) { - $term_update['name'] = $update['post_title']; - } - if (isset($update['post_excerpt'])) { - $term_update['description'] = $update['post_excerpt']; - } - if (!empty($term_update)) { - wp_update_term($term_id, 'aksrt_forum_board', $term_update); - } - if (isset($data['icon'])) { - update_term_meta($term_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - if (isset($data['sort_order'])) { - update_term_meta($term_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - } - - return true; - } - - /** - * Delete plate (with cascade option). - */ - public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return false; - } - - $topic_ids = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'meta_key' => '_aksrt_plate_id', - 'meta_value' => $plate_id, - 'posts_per_page' => -1, - 'fields' => 'ids', - 'post_status' => 'any', - ]); - - if (!empty($topic_ids)) { - if ($cascade === 'move' && $move_to > 0 && $move_to !== $plate_id) { - $target = get_post($move_to); - if ($target && $target->post_type === 'aksrt_forum_plate') { - foreach ($topic_ids as $tid) { - update_post_meta((int) $tid, '_aksrt_plate_id', $move_to); - } - $this->forum->sync_plate_counts($move_to); - } - } else { - foreach ($topic_ids as $tid) { - wp_trash_post((int) $tid); - } - } - } - - // Handle child plates (move to top-level) - $children = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_parent' => $plate_id, - 'posts_per_page' => -1, - 'fields' => 'ids', - 'post_status' => 'any', - ]); - foreach ($children as $child_id) { - wp_update_post(['ID' => (int) $child_id, 'post_parent' => 0]); - } - - // Remove legacy taxonomy term if linked - $term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true); - if ($term_id > 0) { - wp_delete_term($term_id, 'aksrt_forum_board'); - } - - wp_delete_post($plate_id, true); - return true; - } - - /** - * Reorder plates. - */ - public function reorder_plates(array $order_map): bool { - foreach ($order_map as $plate_id => $sort_order) { - $plate_id = (int) $plate_id; - if ($plate_id <= 0) continue; - - wp_update_post(['ID' => $plate_id, 'menu_order' => (int) $sort_order]); - update_post_meta($plate_id, '_aksrt_sort_order', (int) $sort_order); - } - - return true; - } - - /** - * Resolve plate_id for a topic (reads _aksrt_plate_id meta, falls back to taxonomy). - */ - public function get_topic_plate_id(int $topic_id): int { - $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - return $plate_id; - } - - // Fallback: find plate via legacy taxonomy term - $terms = get_the_terms($topic_id, 'aksrt_forum_board'); - if (!$terms || is_wp_error($terms)) { - return 0; - } - - $term = $terms[0]; - // Look up plate post by migrated_from_term meta - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'meta_key' => '_aksrt_migrated_from_term', - 'meta_value' => (string) $term->term_id, - 'numberposts' => 1, - 'fields' => 'ids', - 'post_status' => 'publish', - ]); - - if (!empty($plates)) { - $resolved = (int) $plates[0]; - update_post_meta($topic_id, '_aksrt_plate_id', $resolved); - return $resolved; - } - - return 0; - } - - /** - * Check if a user is a plate moderator (CPT-based). - */ - public function is_plate_moderator(int $user_id, int $plate_id): bool { - if ($user_id <= 0 || $plate_id <= 0) { - return false; - } - - if ($this->forum->is_forum_admin($user_id)) { - return true; - } - - $post = get_post($plate_id); - if ($post && (int) $post->post_author === $user_id) { - return true; - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($moderators) && in_array($user_id, $moderators, true)) { - return true; - } - - return false; - } - - /** - * Get plate view (topics + moderators + pagination) for frontend. - */ - public function get_plate_view(int $plate_id, array $args = []): array { - $plate = $this->get_plate($plate_id); - if (!$plate) { - return [ - 'plate' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'children' => [], - ]; - } - - $user_id = (int) ($args['user_id'] ?? get_current_user_id()); - $paged = max(1, (int) ($args['paged'] ?? 1)); - - $topics = $this->forum->get_topic_list([ - 'plate_id' => $plate_id, - 'page' => $paged, - 'limit' => 20, - ]); - - // Global + super + plate sticky - $sticky_topics = $this->get_sticky_topics_for_plate($plate_id); - - $children = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_parent' => $plate_id, - 'post_status' => 'publish', - 'posts_per_page' => 50, - 'orderby' => 'menu_order', - 'order' => 'ASC', - ]); - - $child_plates = []; - foreach ($children as $child) { - $child_plates[] = $this->build_plate_payload($child); - } - - return [ - 'plate' => $plate, - 'topics' => $topics['items'], - 'sticky_topics' => $sticky_topics, - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id), - 'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'), - 'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id), - 'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id), - 'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id), - 'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id), - ], - 'children' => $child_plates, - ]; - } - - /** - * Get sticky topics for a plate (global + super + plate-level). - */ - private function get_sticky_topics_for_plate(int $plate_id): array { - $global_sticky = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'meta_key' => '_aksrt_sticky', - 'meta_value' => Forum::STICKY_GLOBAL, - 'posts_per_page' => 5, - 'orderby' => 'date', - 'order' => 'DESC', - ]); - - $plate_sticky = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'posts_per_page' => 10, - 'orderby' => 'date', - 'order' => 'DESC', - 'meta_query' => [ - 'relation' => 'AND', - ['key' => '_aksrt_plate_id', 'value' => $plate_id, 'type' => 'NUMERIC'], - ['key' => '_aksrt_sticky', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'], - ['key' => '_aksrt_sticky', 'value' => Forum::STICKY_GLOBAL, 'compare' => '<', 'type' => 'NUMERIC'], - ], - ]); - - $items = []; - $seen = []; - - foreach (array_merge($global_sticky, $plate_sticky) as $post) { - if (isset($seen[$post->ID])) continue; - $seen[$post->ID] = true; - $items[] = $this->forum->build_topic_payload($post); - } - - return array_slice($items, 0, 10); - } - - /** - * Get moderator user profiles for a plate. - */ - private function get_plate_moderator_profiles(int $plate_id): array { - $post = get_post($plate_id); - $profiles = []; - - // Author = super moderator - if ($post && $post->post_author > 0) { - $user = get_userdata((int) $post->post_author); - if ($user) { - $profiles[] = [ - 'id' => (int) $user->ID, - 'name' => $user->display_name, - 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user->ID, 40) : get_avatar_url($user->ID, ['size' => 40]), - 'role' => 'plate_author', - ]; - } - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return $profiles; - } - - $roles = get_post_meta($plate_id, '_aksrt_moderator_roles', true); - if (!is_array($roles)) { - $roles = []; - } - - foreach ($moderators as $uid) { - $uid = (int) $uid; - if ($post && $uid === (int) $post->post_author) continue; - - $user = get_userdata($uid); - if (!$user) continue; - - $profiles[] = [ - 'id' => $uid, - 'name' => $user->display_name, - 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($uid, 40) : get_avatar_url($uid, ['size' => 40]), - 'role' => (string) ($roles[$uid] ?? 'moderator'), - ]; - } - - return $profiles; - } -} diff --git a/src/AKSRT/Forum/BoardServicePart2.php b/src/AKSRT/Forum/BoardServicePart2.php deleted file mode 100644 index 87bbce8d..00000000 --- a/src/AKSRT/Forum/BoardServicePart2.php +++ /dev/null @@ -1,796 +0,0 @@ - '全部', 'sort' => 'date'], - ['label' => '推荐', 'sort' => 'featured'], - ['label' => '最新回复', 'sort' => 'last_reply'], - ['label' => '热门', 'sort' => 'hot'], - ['label' => '精华', 'sort' => 'essence'], - ]; - $allowed_sorts = []; - foreach ($tabs as $tab) { - $sort = sanitize_key((string) ($tab['sort'] ?? '')); - if ($sort !== '') { - $allowed_sorts[] = $sort; - } - } - if ($orderby === '' || !in_array($orderby, $allowed_sorts, true)) { - $active_index = max(0, (int) ($board_page['tab_active_index'] ?? 0)); - $orderby = sanitize_key((string) ($tabs[$active_index]['sort'] ?? 'date')); - if ($orderby === '') { - $orderby = 'date'; - } - } - - $compose_url = function_exists('aksrt_get_forum_compose_url') - ? aksrt_get_forum_compose_url(['board' => $board_id]) - : home_url('/'); - $top_info = !isset($board_page['top_info']) || !empty($board_page['top_info']); - - if ($this->forum->is_plate_cpt_active()) { - $view = $this->get_plate_view_compat($board_id, $user_id, $page, $orderby); - $view['tabs'] = $tabs; - $view['active_sort'] = $orderby; - $view['top_info'] = $top_info; - $view['compose_url'] = $compose_url; - return $view; - } - - $board = get_term($board_id, 'aksrt_forum_board'); - - if (!$board || is_wp_error($board)) { - return [ - 'board' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'application' => null, - 'board_options' => [], - 'tabs' => $tabs, - 'active_sort' => $orderby, - 'top_info' => $top_info, - 'compose_url' => function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : home_url('/'), - ]; - } - - $topics = $this->forum->get_topic_list([ - 'board_id' => $board_id, - 'page' => $page, - 'limit' => 20, - 'status' => 'publish', - 'viewer_user_id' => $user_id, - 'orderby' => $orderby, - ]); - - $sticky_topics = $this->forum->get_sticky_topics_for_board($board_id); - $board_link = get_term_link($board); - - return [ - 'board' => [ - 'id' => $board_id, - 'name' => (string) $board->name, - 'description' => (string) $board->description, - 'content' => '', - 'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true), - 'topic_count' => (int) $board->count, - 'reply_count' => 0, - 'today_reply_count' => 0, - 'views' => 0, - 'follow_count' => 0, - 'last_reply' => '', - 'last_post' => '', - 'url' => !is_wp_error($board_link) ? (string) $board_link : '', - 'thumbnail_url' => '', - 'author_id' => 0, - 'allow_view' => 'public', - ], - 'topics' => $topics['items'], - 'sticky_topics' => array_slice($sticky_topics, 0, 5), - 'moderators' => $this->forum->get_board_moderator_profiles($board_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0, - 'can_apply_moderator' => $user_id > 0 && !$this->forum->is_board_moderator($user_id, $board_id), - 'can_moderate' => $this->forum->is_board_moderator($user_id, $board_id), - ], - 'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $board_id) : null, - 'board_options' => $this->get_boards(), - 'tabs' => $tabs, - 'active_sort' => $orderby, - 'top_info' => $top_info, - 'compose_url' => $compose_url, - ]; - } - - /** - * Plate CPT board view (backwards-compatible with get_board_view return shape). - */ - private function get_plate_view_compat(int $plate_id, int $user_id, int $page, string $orderby = 'date'): array { - $plate = $this->get_plate($plate_id); - if (!$plate) { - return [ - 'board' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'application' => null, - 'board_options' => [], - ]; - } - - // Check access permission - $allow_view = $plate['allow_view'] ?? 'public'; - if (!$this->can_view_plate($user_id, $plate_id, $allow_view)) { - return [ - 'board' => $plate, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [ - 'can_compose' => false, - 'can_apply_moderator' => false, - 'can_moderate' => false, - 'access_denied' => true, - 'access_reason' => $allow_view, - ], - 'application' => null, - 'board_options' => $this->get_boards(), - ]; - } - - $topics = $this->forum->get_topic_list([ - 'plate_id' => $plate_id, - 'page' => $page, - 'limit' => 20, - 'status' => 'publish', - 'viewer_user_id' => $user_id, - 'orderby' => $orderby, - ]); - - $sticky_topics = $this->get_sticky_topics_for_plate($plate_id); - - return [ - 'board' => $plate, - 'topics' => $topics['items'], - 'sticky_topics' => array_slice($sticky_topics, 0, 5), - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id), - 'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'), - 'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id), - 'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id), - 'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id), - 'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id), - ], - 'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $plate_id) : null, - 'board_options' => $this->get_boards(), - ]; - } - - /** - * Check if a user can view a plate (§7.2 板块阅读权限). - */ - public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool { - if ($allow_view === '') { - $allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'); - } - - if ($allow_view === 'public') { - return true; - } - - if ($user_id <= 0) { - // Guest cash follow unlock via signed cookie (money follow pay only). - if ($allow_view === 'follow' - && function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id) - ) { - return true; - } - return false; - } - - if ($this->is_plate_moderator($user_id, $plate_id)) { - return true; - } - - if ($allow_view === 'login') { - return true; - } - - if ($allow_view === 'follow') { - return $this->is_user_following_plate($user_id, $plate_id); - } - - if ($allow_view === 'role') { - $allowed_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true); - if (!is_array($allowed_roles) || empty($allowed_roles)) { - return true; - } - $user = get_userdata($user_id); - if (!$user) { - return false; - } - return !empty(array_intersect($user->roles, $allowed_roles)); - } - - return true; - } - - /** - * Check if a user can compose in a plate (§7.2 板块发帖限制). - */ - public function can_compose_in_plate(int $user_id, int $plate_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_plate_moderator($user_id, $plate_id)) { - return true; - } - - $add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'); - if ($add_limit === 'all' || $add_limit === 'login') { - return true; - } - - // Moderators already returned true above. - if ($add_limit === 'moderator') { - return false; - } - - $user = get_userdata($user_id); - if (!$user) { - return false; - } - - if ($add_limit === 'role') { - $allowed_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); - if (!is_array($allowed_roles) || empty($allowed_roles)) { - return true; - } - return !empty(array_intersect($user->roles, $allowed_roles)); - } - - // Unknown contract → deny (no legacy fallthrough). - return false; - } - - /** - * Check if a user is following a plate (§7.2 关注). - */ - public function is_user_following_plate(int $user_id, int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id); - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - return false; - } - return in_array($plate_id, $follow_list, true); - } - - /** - * Toggle user follow on a plate (§7.2 关注/取消关注). - * - * When plate has paid follow enabled and user is not following, returns - * require_pay payload instead of free-following. - * - * @return array{followed?:bool,follow_count?:int,require_pay?:bool,pay?:array,message?:string,success?:bool} - */ - public function toggle_follow_plate(int $user_id, int $plate_id): array { - if ($user_id <= 0) { - return ['followed' => false, 'follow_count' => 0, 'success' => false, 'message' => __('请先登录', 'aksrt')]; - } - - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - - $followed = in_array($plate_id, $follow_list, true); - if ($followed) { - // Unfollow is always free (zibll warns but still allows cancel). - $follow_list = array_values(array_diff($follow_list, [$plate_id])); - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - return [ - 'followed' => false, - 'follow_count' => $count, - 'success' => true, - ]; - } - - // Follow path — paid gate. - if ($this->is_plate_follow_pay_required($plate_id, $user_id)) { - $price = $this->resolve_plate_follow_pay_price($plate_id, $user_id); - return [ - 'followed' => false, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'require_pay' => true, - 'success' => false, - 'message' => __('关注该版块需要付费', 'aksrt'), - 'pay' => $price, - ]; - } - - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - - return [ - 'followed' => true, - 'follow_count' => $count, - 'success' => true, - ]; - } - - /** - * Whether global+plate paid follow is on and user still needs to pay. - */ - public function is_plate_follow_pay_required(int $plate_id, int $user_id): bool { - if ($user_id <= 0 || $plate_id <= 0) { - return false; - } - if ($this->is_user_following_plate($user_id, $plate_id)) { - return false; - } - if ($this->is_plate_moderator($user_id, $plate_id) || user_can($user_id, 'manage_options')) { - return false; - } - if (!$this->is_plate_follow_pay_enabled($plate_id)) { - return false; - } - // Already purchased plate_follow order. - if (class_exists('\AKSRT\Payment\OrderRepository') && class_exists('\AKSRT\Core\DomainModel')) { - if (\AKSRT\Payment\OrderRepository::get_instance()->has_paid_access( - $user_id, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - $plate_id - )) { - return false; - } - } - return true; - } - - public function is_plate_follow_pay_enabled(int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - // Global switch from forum settings (SPA). - $global_on = true; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $cfg = \AKSRT\Settings\Settings::get_forum_settings(); - // Nested permissions.plate_follow_pay or flat. - if (isset($cfg['permissions']['plate_follow_pay'])) { - $global_on = !empty($cfg['permissions']['plate_follow_pay']); - } elseif (isset($cfg['plate_follow_pay'])) { - $global_on = !empty($cfg['plate_follow_pay']); - } - } - if (!$global_on) { - return false; - } - if ((int) get_post_meta($plate_id, '_aksrt_follow_pay', true) !== 1) { - return false; - } - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if ($modo === 'points') { - return (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true) > 0; - } - return (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true) > 0; - } - - /** - * @return array{amount:float|int,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float|int,currency:string,vip_level:int,vip_set:bool} - */ - public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array { - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - - $is_points = $modo === 'points'; - $base = $is_points - ? max(0, (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true)) - : max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)); - - $vip_level = 0; - if ($user_id > 0) { - if (function_exists('aksrt_get_user_vip_level')) { - $vip_level = (int) aksrt_get_user_vip_level($user_id); - } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { - $vip_level = 2; - } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { - $vip_level = 1; - } - if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { - $vip_level = 1; - } - } - - $pick = static function ($raw) use ($is_points) { - if ($raw === '' || $raw === null || $raw === false) { - return null; - } - if (!is_numeric($raw)) { - return null; - } - return $is_points ? max(0, (int) $raw) : max(0.0, (float) $raw); - }; - - $vip2 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true)); - $vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true)); - if ($vip1 === null) { - // Legacy single VIP key → vip1. - $vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true)); - } - - $amount = $base; - $is_vip_price = false; - $vip_set = ($vip1 !== null || $vip2 !== null); - - if ($vip_level >= 2 && $vip2 !== null) { - $amount = $vip2; - $is_vip_price = true; - } elseif ($vip_level >= 1 && $vip1 !== null) { - $amount = $vip1; - $is_vip_price = true; - } elseif ($vip_level >= 2 && $vip1 !== null) { - // VIP2 user falls back to VIP1 when VIP2 unset. - $amount = $vip1; - $is_vip_price = true; - } - - if ($is_vip_price) { - return [ - 'amount' => $amount, - 'modo' => $modo, - 'is_vip_price' => true, - 'is_free' => $amount <= 0, - 'label' => $amount <= 0 - ? __('VIP 免费关注', 'aksrt') - : ($is_points - ? sprintf(__('VIP %d 积分', 'aksrt'), (int) $amount) - : sprintf(__('VIP ¥%s', 'aksrt'), number_format((float) $amount, 2, '.', ''))), - 'base' => $base, - 'currency' => $is_points ? 'points' : 'CNY', - 'vip_level' => $vip_level, - 'vip_set' => true, - ]; - } - - return [ - 'amount' => $base, - 'modo' => $modo, - 'is_vip_price' => false, - 'is_free' => false, - 'label' => $is_points - ? sprintf(__('%d 积分', 'aksrt'), (int) $base) - : sprintf(__('¥%s', 'aksrt'), number_format((float) $base, 2, '.', '')), - 'base' => $base, - 'currency' => $is_points ? 'points' : 'CNY', - 'vip_level' => $vip_level, - 'vip_set' => $vip_set, - ]; - } - - /** - * Force-add plate to user follow list (after paid success). - */ - public function mark_plate_followed(int $user_id, int $plate_id): void { - if ($user_id <= 0 || $plate_id <= 0) { - return; - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - } - - /** - * Purchase plate follow with points or balance (creates plate_follow order when cash). - * - * @return array{success:bool,message:string,followed?:bool,follow_count?:int,order_number?:string} - */ - public function purchase_plate_follow(int $user_id, int $plate_id): array { - if ($user_id <= 0) { - return $this->forum->error(__('请先登录', 'aksrt')); - } - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return $this->forum->error(__('版块不存在', 'aksrt')); - } - if ($this->is_user_following_plate($user_id, $plate_id)) { - return $this->forum->success(__('已关注', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - ]); - } - if (!$this->is_plate_follow_pay_enabled($plate_id)) { - // Free follow fallback. - $this->mark_plate_followed($user_id, $plate_id); - return $this->forum->success(__('关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - ]); - } - - $price = $this->resolve_plate_follow_pay_price($plate_id, $user_id); - if (!empty($price['is_free'])) { - $this->mark_plate_followed($user_id, $plate_id); - return $this->forum->success(__('VIP 免费关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'free' => true, - ]); - } - - if (!class_exists('\AKSRT\Credits\Credits')) { - return $this->forum->error(__('积分系统不可用', 'aksrt')); - } - - $modo = (string) ($price['modo'] ?? 'money'); - $amount = $price['amount'] ?? 0; - - if ($modo === 'points') { - $cost = max(0, (int) $amount); - if ($cost <= 0) { - return $this->forum->error(__('积分价格未设置', 'aksrt')); - } - $balance = \AKSRT\Credits\Credits::get_balance($user_id); - if ($balance < $cost) { - return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost)); - } - $deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'forum_plate_follow', [ - 'plate_id' => $plate_id, - ], sprintf(__('付费关注版块「%s」', 'aksrt'), get_the_title($plate_id))); - if (is_wp_error($deducted) || $deducted === false) { - $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt'); - return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt')); - } - $this->mark_plate_followed($user_id, $plate_id); - $this->grant_plate_follow_author_share($user_id, $plate_id, $cost, true); - return $this->forum->success(__('关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'credits_cost' => $cost, - ]); - } - - // Cash: multi-gateway cashier only (PlateFollowPay REST). No Credits×100 bypass. - return $this->forum->error(__('请通过收银台支付关注费用。', 'aksrt'), [ - 'require_cashier' => true, - 'pay' => $price, - 'modo' => 'money', - 'plate_id' => $plate_id, - ]); - } - - private function grant_plate_follow_author_share(int $buyer_id, int $plate_id, int $cost, bool $is_points): void { - if (!$is_points || $cost <= 0 || !class_exists('\AKSRT\Credits\Credits')) { - return; - } - $post = get_post($plate_id); - if (!$post) { - return; - } - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $buyer_id) { - return; - } - $author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70)); - $author_amount = (int) floor($cost * $author_share / 100); - if ($author_amount <= 0) { - return; - } - \AKSRT\Credits\Credits::add($author_id, $author_amount, 'forum_plate_follow_income', [ - 'plate_id' => $plate_id, - 'buyer_id' => $buyer_id, - ], sprintf(__('版块「%s」被付费关注', 'aksrt'), get_the_title($plate_id))); - } - - /** - * Count followers for a plate. - */ - private function count_plate_followers(int $plate_id): int { - global $wpdb; - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->usermeta} - WHERE meta_key = 'aksrt_follow_plate' - AND meta_value LIKE %s", - '%i:' . $plate_id . ';%' - )); - } - - /** - * Get plates a user follows. - */ - public function get_user_followed_plates(int $user_id): array { - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list) || empty($follow_list)) { - return []; - } - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post__in' => array_map('intval', $follow_list), - 'post_status' => 'publish', - 'posts_per_page' => 100, - 'orderby' => 'post__in', - ]); - - $result = []; - foreach ($plates as $plate) { - $result[] = $this->build_plate_payload($plate); - } - return $result; - } - - /** - * Get a user's managed plates (plates where user is moderator/author). - * - * @return array{owned:array,moderating:array} - */ - public function get_user_managed_plates(int $user_id): array { - $owned = []; - $moderating = []; - - if (!$this->forum->is_plate_cpt_active()) { - return ['owned' => $owned, 'moderating' => $moderating]; - } - - // Plates where user is author (超级版主) - $own_plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'author' => $user_id, - 'post_status' => 'publish', - 'posts_per_page' => 50, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - foreach ($own_plates as $plate) { - $owned[] = $this->build_plate_payload($plate); - } - - // Plates where user is a moderator (in _aksrt_moderators list) - global $wpdb; - $mod_plate_ids = $wpdb->get_col($wpdb->prepare( - "SELECT pm.post_id FROM {$wpdb->postmeta} pm - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'aksrt_forum_plate' AND p.post_status = 'publish' - WHERE pm.meta_key = %s", - '_aksrt_moderators' - )); - - foreach ($mod_plate_ids as $plate_id) { - $plate_id = (int) $plate_id; - // Skip plates already in owned - if (in_array($plate_id, wp_list_pluck($own_plates, 'ID'), true)) { - continue; - } - $mods = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - $plate = get_post($plate_id); - if ($plate) { - $moderating[] = $this->build_plate_payload($plate); - } - } - } - - return ['owned' => $owned, 'moderating' => $moderating]; - } - - // ── Plate utilities (extracted from facade) ── - - public function increment_plate_views(int $plate_id): void { - if ($plate_id <= 0 || !$this->forum->is_plate_cpt_active()) { - return; - } - $views = (int) get_post_meta($plate_id, '_aksrt_views', true); - update_post_meta($plate_id, '_aksrt_views', $views + 1); - } - - /** - * Check if a plate qualifies as "hot" (§7.2 热门版块). - */ - public function is_hot_plate(int $plate_id): bool { - $config = $this->forum->get_config(); - $threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100); - $posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true); - $follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true); - $is_hot = ($posts_count + $follow_count * 2) >= $threshold; - if ($is_hot && get_post_meta($plate_id, '_aksrt_level_hot_awarded', true) !== '1') { - $plate = get_post($plate_id); - $author_id = $plate instanceof \WP_Post ? (int) $plate->post_author : 0; - update_post_meta($plate_id, '_aksrt_level_hot_awarded', '1'); - do_action('aksrt_forum_plate_hot', $plate_id, $author_id); - } - return $is_hot; - } - - /** - * Get plate categories (分区) with moderators. - */ - public function get_plate_categories(): array { - $terms = get_terms([ - 'taxonomy' => 'aksrt_plate_cat', - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ]); - - if (is_wp_error($terms) || empty($terms)) { - return []; - } - - $categories = []; - foreach ($terms as $term) { - $term_id = (int) $term->term_id; - $moderator_ids = get_term_meta($term_id, '_aksrt_cat_moderator', true); - if (!is_array($moderator_ids)) { - $moderator_ids = []; - } - - $categories[] = [ - 'id' => $term_id, - 'name' => (string) $term->name, - 'description' => (string) $term->description, - 'slug' => (string) $term->slug, - 'count' => (int) $term->count, - 'parent_id' => (int) $term->parent, - 'moderators' => array_values(array_unique(array_filter(array_map('intval', $moderator_ids)))), - 'add_limit' => (string) (get_term_meta($term_id, '_aksrt_add_limit', true) ?: 'all'), - 'url' => (string) get_term_link($term), - ]; - } - - return $categories; - } -} diff --git a/src/AKSRT/Forum/BoardServicePart3.php b/src/AKSRT/Forum/BoardServicePart3.php deleted file mode 100644 index 85c97f97..00000000 --- a/src/AKSRT/Forum/BoardServicePart3.php +++ /dev/null @@ -1,187 +0,0 @@ -forum->is_forum_admin($user_id)) { - return true; - } - $moderator_ids = get_term_meta($cat_id, '_aksrt_cat_moderator', true); - if (!is_array($moderator_ids)) { - return false; - } - return in_array($user_id, array_map('intval', $moderator_ids), true); - } - - /** - * Get plates sorted by a specified dimension (§7.2 多维排序). - */ - public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array { - $allowed = [ - 'name' => 'title', - 'date' => 'date', - 'modified' => 'modified', - 'menu_order' => 'menu_order', - 'last_post' => '_aksrt_last_post', - 'last_reply' => '_aksrt_last_reply', - 'posts_count' => '_aksrt_posts_count', - 'reply_count' => '_aksrt_reply_count', - 'today_reply_count' => '_aksrt_today_reply_count', - 'views' => '_aksrt_views', - 'follow_count' => '_aksrt_follow_count', - 'rand' => 'rand', - ]; - - $query_args = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => $limit, - 'order' => strtoupper($order) === 'ASC' ? 'ASC' : 'DESC', - ]; - - if (isset($allowed[$orderby])) { - $meta_key = $allowed[$orderby]; - if (str_starts_with($meta_key, '_aksrt_')) { - $query_args['orderby'] = 'meta_value_num'; - $query_args['meta_key'] = $meta_key; - } else { - $query_args['orderby'] = $meta_key; - } - } else { - $query_args['orderby'] = 'menu_order'; - } - - $posts = get_posts($query_args); - $plates = []; - foreach ($posts as $post) { - $plates[] = $this->build_plate_payload($post); - } - return $plates; - } - - public function maybe_migrate_boards(): void { - if (get_option('aksrt_forum_boards_migrated')) { - return; - } - - $old_boards = get_option('aksrt_forum_boards', []); - if (!is_array($old_boards) || empty($old_boards)) { - update_option('aksrt_forum_boards_migrated', time()); - return; - } - - foreach ($old_boards as $board) { - $name = sanitize_text_field((string) ($board['name'] ?? '')); - if ($name === '') { - continue; - } - - $existing = get_terms([ - 'taxonomy' => 'aksrt_forum_board', - 'name' => $name, - 'hide_empty' => false, - 'fields' => 'ids', - ]); - - if (!is_wp_error($existing) && !empty($existing)) { - $term_id = (int) $existing[0]; - } else { - $result = wp_insert_term($name, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($board['description'] ?? '')), - ]); - if (is_wp_error($result)) { - continue; - } - $term_id = (int) $result['term_id']; - } - - $icon = sanitize_text_field((string) ($board['icon'] ?? '')); - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - - update_term_meta($term_id, '_aksrt_sort_order', (int) ($board['sort_order'] ?? $term_id)); - - if (!empty($board['moderators']) && is_array($board['moderators'])) { - update_term_meta($term_id, '_aksrt_moderators', array_map('intval', $board['moderators'])); - } - } - - update_option('aksrt_forum_boards_migrated', time()); - } - - public function ajax_follow_plate(): void { - check_ajax_referer('aksrt_forum_action', '_nonce'); - - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $plate_id = (int) ($_POST['plate_id'] ?? 0); - if ($plate_id <= 0) { - wp_send_json_error(['message' => __('版块不存在', 'aksrt')]); - } - - $result = $this->toggle_follow_plate($user_id, $plate_id); - if (!empty($result['require_pay'])) { - wp_send_json_error([ - 'code' => 'require_pay', - 'message' => (string) ($result['message'] ?? __('需要付费关注', 'aksrt')), - 'require_pay' => true, - 'pay' => $result['pay'] ?? [], - 'followed' => false, - 'follow_count' => (int) ($result['follow_count'] ?? 0), - ]); - } - if (isset($result['success']) && $result['success'] === false) { - wp_send_json_error([ - 'message' => (string) ($result['message'] ?? __('操作失败', 'aksrt')), - ]); - } - wp_send_json_success($result); - } - - public function ajax_purchase_plate_follow(): void { - check_ajax_referer('aksrt_forum_action', '_nonce'); - - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $plate_id = (int) ($_POST['plate_id'] ?? 0); - if ($plate_id <= 0) { - wp_send_json_error(['message' => __('版块不存在', 'aksrt')]); - } - - $result = $this->purchase_plate_follow($user_id, $plate_id); - if (empty($result['success'])) { - $err = [ - 'message' => (string) ($result['message'] ?? __('支付失败', 'aksrt')), - ]; - if (!empty($result['require_cashier'])) { - $err['require_cashier'] = true; - $err['pay'] = $result['pay'] ?? []; - $err['modo'] = (string) ($result['modo'] ?? 'money'); - $err['plate_id'] = (int) ($result['plate_id'] ?? $plate_id); - } - wp_send_json_error($err); - } - wp_send_json_success($result); - } -} diff --git a/src/AKSRT/Forum/CommentService.php b/src/AKSRT/Forum/CommentService.php deleted file mode 100644 index eacae583..00000000 --- a/src/AKSRT/Forum/CommentService.php +++ /dev/null @@ -1,604 +0,0 @@ -forum = $forum; - } - - // ── Hook: forum comment posted ─────────────────────────────────── - - public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void { - $comment = get_comment($comment_id); - if (!$comment) return; - $post = get_post((int) $comment->comment_post_ID); - if (!$post || $post->post_type !== 'aksrt_forum_topic') return; - if ((int) $comment->comment_approved !== 1) return; - $commenter_id = (int) $comment->user_id; - if ($commenter_id <= 0) return; - - // Topic-level last_reply for board sort tabs. - update_post_meta((int) $post->ID, '_aksrt_last_reply', (string) $comment->comment_date); - - $this->forum->notify_topic_reply((int) $post->ID, $commenter_id); - } - - // ── AJAX: get forum comments ───────────────────────────────────── - - /** - * AJAX: Get forum comments with sort/filter/pagination. - */ - public function ajax_forum_comments(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $topic_id = (int) ($_POST['topic_id'] ?? 0); - $sort = sanitize_key((string) ($_POST['sort'] ?? 'newest')); - $sort = in_array($sort, ['newest', 'oldest', 'hottest'], true) ? $sort : 'newest'; - $author_only = !empty($_POST['author_only']); - $page = max(1, (int) ($_POST['page'] ?? 1)); - $per_page = max(1, min(100, (int) ($_POST['per_page'] ?? 30))); - - if ($topic_id <= 0) { - wp_send_json_error(['message' => __('参数无效', 'aksrt')]); - } - - $result = $this->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page); - - ob_start(); - $this->render_forum_comments($result['comments'], $topic_id, $result['adopted_id']); - $html = ob_get_clean(); - - wp_send_json_success([ - 'html' => $html, - 'total' => $result['total'], - 'max_pages' => $result['max_pages'], - 'page' => $result['page'], - 'has_more' => $result['page'] < $result['max_pages'], - ]); - } - - // ── AJAX: comment topping ──────────────────────────────────────── - - /** - * AJAX: Set comment topping level (moderator action). - */ - public function ajax_comment_topping(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $comment_id = (int) ($_POST['comment_id'] ?? 0); - $level = max(0, min(3, (int) ($_POST['level'] ?? 0))); - $user_id = get_current_user_id(); - - $comment = get_comment($comment_id); - if (!$comment) { - wp_send_json_error(['message' => __('评论不存在', 'aksrt')]); - } - - $topic_id = (int) $comment->comment_post_ID; - if (!$this->forum->forum_user_can($user_id, 'reply_top', $topic_id)) { - wp_send_json_error(['message' => __('权限不足', 'aksrt')]); - } - - if ($this->set_comment_topping($comment_id, $level)) { - wp_send_json_success([ - 'message' => $level > 0 ? __('已置顶', 'aksrt') : __('已取消置顶', 'aksrt'), - 'level' => $level, - ]); - } - - wp_send_json_error(['message' => __('操作失败', 'aksrt')]); - } - - // ── Render forum comments ──────────────────────────────────────── - - /** - * Render forum comments HTML for AJAX and initial load. - * - * @param \WP_Comment[] $comments Comments (parents + children mixed). - * @param int $topic_id Topic post ID. - * @param int $adopted_id Adopted comment ID (0 if none). - */ - public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void { - $post = get_post($topic_id); - $post_author_id = $post ? (int) $post->post_author : 0; - $current_user_id = get_current_user_id(); - $is_question = 'question' === (string) get_post_meta($topic_id, '_aksrt_bbs_type', true); - $can_adopt = $is_question && $current_user_id > 0 - && ($current_user_id === $post_author_id || $this->forum->forum_user_can($current_user_id, 'reply_adopt_other', $topic_id)); - $can_moderate = $this->forum->forum_user_can($current_user_id, 'topic_moderate', $topic_id); - - if (empty($comments)) { - echo '
' . esc_html__('暂无回复,来抢沙发吧!', 'aksrt') . '
'; - return; - } - - foreach ($comments as $comment) { - if (!($comment instanceof \WP_Comment)) continue; - if ((int) $comment->comment_parent !== 0) continue; - - $comment_id = (int) $comment->comment_ID; - $commenter_uid = (int) $comment->user_id; - $is_adopted = $comment_id === $adopted_id; - $is_hot = $this->is_hot_comment($comment_id); - $is_post_author = $commenter_uid > 0 && $commenter_uid === $post_author_id; - $topping = (int) get_comment_meta($comment_id, '_aksrt_comment_topping', true); - $badge = $this->get_commenter_badge($commenter_uid, $topic_id); - - $like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0; - $is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($comment_id) : false; - - $classes = ['aksrt-forum-reply']; - if ($is_adopted) $classes[] = 'is-adopted'; - if ($is_hot) $classes[] = 'is-hot'; - if ($topping > 0) $classes[] = 'is-pinned'; - ?> -
-
-
-
- - - - - - - - 0) : ?> - - - - - - - - - -
- -
-
- - - - - - - - - - - - - -
- - - comment_parent === $comment_id; - }); - if (!empty($child_comments)) : ?> -
- comment_ID; - $child_uid = (int) $child->user_id; - $child_badge = $this->get_commenter_badge($child_uid, $topic_id); - $child_is_author = $child_uid > 0 && $child_uid === $post_author_id; - $child_like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($child_id) : 0; - $child_is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($child_id) : false; - $parent_comment = get_comment((int) $child->comment_parent); - $parent_name = $parent_comment ? get_comment_author((int) $parent_comment->comment_ID) : ''; - ?> -
-
-
-
- - - - - - - - comment_parent !== $comment_id) : ?> - - - -
-
-
- - - - -
-
-
- -
- -
-
- post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); - if ($bbs_type !== 'question') { - return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); - } - - $author_id = (int) $post->post_author; - if ($operator_id !== $author_id && !$this->forum->can_moderate_topic($operator_id, $topic_id)) { - return new \WP_Error('forbidden', __('仅提问者或版主可采纳答案', 'aksrt')); - } - - $comment = get_comment($comment_id); - if (!$comment || (int) $comment->comment_post_ID !== $topic_id) { - return new \WP_Error('invalid_comment', __('回复不存在', 'aksrt')); - } - - $existing = get_post_meta($topic_id, '_aksrt_adopted_comment', true); - if ($existing && (int) $existing === $comment_id) { - return new \WP_Error('already_adopted', __('该回复已被采纳', 'aksrt')); - } - - if ($existing) { - delete_comment_meta((int) $existing, '_aksrt_adopted'); - } - - update_comment_meta($comment_id, '_aksrt_adopted', '1'); - update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id); - do_action('aksrt_forum_reply_adopted', $topic_id, $comment_id, (int) $comment->user_id); - update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED - - $adoptee_id = (int) $comment->user_id; - if ($adoptee_id > 0) { - $config = $this->forum->get_config(); - $amount = (int) ($config['credits']['adopted'] ?? 20); - if ($amount > 0) { - $already = get_comment_meta($comment_id, '_aksrt_adopt_credited', true); - if (!$already) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - $granted = \AKSRT\Credits\PointsFree::grant($adoptee_id, 'bbs_adopt', $amount, [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - if (!is_wp_error($granted) && (int) $granted > 0) { - update_comment_meta($comment_id, '_aksrt_adopt_credited', '1'); - } - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($adoptee_id, $amount, 'forum_adopted', [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - update_comment_meta($comment_id, '_aksrt_adopt_credited', '1'); - } - } - } - - $this->forum->notify($adoptee_id, 'system', sprintf( - __('你的回复被采纳为最佳答案(帖子「%s」)', 'aksrt'), - get_the_title($topic_id) - ), [ - 'title' => __('回复被采纳', 'aksrt'), - 'target_url' => get_permalink($topic_id) . '#comment-' . $comment_id, - ]); - } - - return [ - 'message' => __('已采纳该回复为最佳答案', 'aksrt'), - 'comment_id' => $comment_id, - 'solved' => true, - ]; - } - - - - public function get_adopted_comment_id(int $topic_id): int { - return (int) get_post_meta($topic_id, '_aksrt_adopted_comment', true); - } - - // ── Get forum comments (query) ────────────────────────────────── - - /** - * Get forum comments with sort/filter support. - * - * @param int $topic_id Topic post ID. - * @param string $sort 'newest'|'oldest'|'hottest' - * @param bool $author_only Only show post-author replies. - * @param int $page Page number. - * @param int $per_page Items per page. - * @return array{comments:\WP_Comment[],total:int,max_pages:int,page:int,adopted_id:int} - */ - public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return ['comments' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1, 'adopted_id' => 0]; - } - - $args = [ - 'post_id' => $topic_id, - 'status' => 'approve', - 'parent' => 0, - 'number' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]; - - if ($author_only) { - $args['user_id'] = (int) $post->post_author; - } - - if ($sort === 'hottest') { - $args['meta_key'] = 'aksrt_comment_likes'; - $args['orderby'] = 'meta_value_num'; - $args['order'] = 'DESC'; - } elseif ($sort === 'oldest') { - $args['orderby'] = 'comment_date_gmt'; - $args['order'] = 'ASC'; - } else { - $args['orderby'] = 'comment_date_gmt'; - $args['order'] = 'DESC'; - } - - // Total count for pagination - $count_args = $args; - unset($count_args['number'], $count_args['offset']); - $count_args['count'] = true; - $total = (int) get_comments($count_args); - - $top_level = get_comments($args); - - - // Fetch child comments for these parents - if (!empty($top_level)) { - $parent_ids = wp_list_pluck($top_level, 'comment_ID'); - $children = get_comments([ - 'post_id' => $topic_id, - 'status' => 'approve', - 'parent__in' => $parent_ids, - 'orderby' => 'comment_date_gmt', - 'order' => 'ASC', - 'number' => 200, - ]); - } else { - $children = []; - } - - // Float pinned top-level comments - $pinned = []; - $normal = []; - foreach ($top_level as $c) { - $topping = (int) get_comment_meta((int) $c->comment_ID, '_aksrt_comment_topping', true); - if ($topping > 0) { - $c->aksrt_topping = $topping; - $pinned[] = $c; - } else { - $normal[] = $c; - } - } - usort($pinned, function ($a, $b) { - return ($b->aksrt_topping ?? 0) <=> ($a->aksrt_topping ?? 0); - }); - $comments = array_merge($pinned, $normal, $children); - - return [ - 'comments' => $comments, - 'total' => $total, - 'max_pages' => max(1, (int) ceil($total / $per_page)), - 'page' => $page, - 'adopted_id' => $this->get_adopted_comment_id($topic_id), - ]; - } - - // ── Set comment topping ───────────────────────────────────────── - - /** - * Set comment topping level. - * - * @param int $comment_id Comment ID. - * @param int $level 0=unpin, 1=normal pin, 2=hot pin, 3=best answer pin. - */ - public function set_comment_topping(int $comment_id, int $level): bool { - $comment = get_comment($comment_id); - if (!$comment) { - return false; - } - if ($level <= 0) { - delete_comment_meta($comment_id, '_aksrt_comment_topping'); - delete_comment_meta($comment_id, '_aksrt_comment_pinned'); - } else { - update_comment_meta($comment_id, '_aksrt_comment_topping', $level); - update_comment_meta($comment_id, '_aksrt_comment_pinned', '1'); - } - return true; - } - - - - // ── Maybe mark hot comment ────────────────────────────────────── - - /** - * Check and mark a comment as hot ("神评") if it crosses the like threshold. - * - * @return bool True if newly marked as hot. - */ - public function maybe_mark_hot_comment(int $comment_id): bool { - $comment = get_comment($comment_id); - if (!$comment || '1' !== (string) $comment->comment_approved) { - return false; - } - - $post = get_post((int) $comment->comment_post_ID); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return false; - } - - $already_hot = '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); - if ($already_hot) { - return false; - } - - $config = $this->forum->get_config(); - $threshold = max(1, (int) ($config['reply']['hot_threshold'] ?? 10)); - $likes = max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); - - if ($likes < $threshold) { - return false; - } - - update_comment_meta($comment_id, '_aksrt_is_hot', '1'); - $hot_author = (int) (get_comment($comment_id)->user_id ?? 0); - do_action('aksrt_forum_comment_hot', $comment_id, $hot_author); - - // Credit the comment author (free-task day cap). - $author_id = (int) $comment->user_id; - if ($author_id > 0) { - $amount = (int) ($config['credits']['comment_hot'] ?? $config['credits']['comment_like'] ?? 5); - if ($amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_comment_hot', $amount, [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_comment', [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - } - } - } - - // Notify the author - if ($author_id > 0) { - $this->forum->notify($author_id, 'system', sprintf( - __('你的回复成为了神评(帖子「%s」)', 'aksrt'), - get_the_title((int) $comment->comment_post_ID) - ), [ - 'title' => __('恭喜获得神评', 'aksrt'), - 'target_url' => get_permalink((int) $comment->comment_post_ID) . '#comment-' . $comment_id, - ]); - } - - return true; - } - - // ── Is hot comment ────────────────────────────────────────────── - - /** - * Check if a comment is a hot comment ("神评"). - */ - public function is_hot_comment(int $comment_id): bool { - return '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); - } - - // ── Get commenter badge ───────────────────────────────────────── - - /** - * Get the user's moderator tier label for badge rendering in comments. - * - * @return array{tier:int,label:string,class:string}|null - */ - public function get_commenter_badge(int $user_id, int $topic_id): ?array { - if ($user_id <= 0) { - return null; - } - - if ($this->forum->is_forum_admin($user_id)) { - return [ - 'tier' => Forum::TIER_FORUM_ADMIN, - 'label' => __('管理员', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--admin', - ]; - } - - $plate_id = $this->forum->get_topic_plate_id($topic_id); - if ($plate_id > 0) { - $tier = $this->forum->get_user_plate_tier($user_id, $plate_id); - if ($tier >= Forum::TIER_CAT_MOD) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['cat_moderator'] ?? __('分区版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--catmod', - ]; - } - if ($tier >= Forum::TIER_PLATE_AUTHOR) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['plate_author'] ?? __('超级版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--super', - ]; - } - if ($tier >= Forum::TIER_MODERATOR) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['plate_moderator'] ?? __('版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--mod', - ]; - } - } - - return null; - } -} diff --git a/src/AKSRT/Forum/DraftService.php b/src/AKSRT/Forum/DraftService.php deleted file mode 100644 index 8d70e894..00000000 --- a/src/AKSRT/Forum/DraftService.php +++ /dev/null @@ -1,441 +0,0 @@ -forum = $forum; - } - - public function save_draft(int $user_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $draft_id = (int) ($data['draft_id'] ?? 0); - - $post_arr = [ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'draft', - 'post_author' => $user_id, - 'post_title' => sanitize_text_field((string) ($data['title'] ?? '')), - 'post_content' => wp_kses_post((string) ($data['content'] ?? '')), - ]; - - if ($draft_id > 0) { - $existing = get_post($draft_id); - if (!$existing || $existing->post_type !== 'aksrt_forum_topic' - || (int) $existing->post_author !== $user_id - || $existing->post_status !== 'draft') { - return new \WP_Error('invalid_draft', __('草稿不存在或无权编辑', 'aksrt')); - } - $post_arr['ID'] = $draft_id; - $result = wp_update_post($post_arr, true); - } else { - $result = wp_insert_post($post_arr, true); - } - - if (is_wp_error($result)) { - return $result; - } - - $post_id = (int) $result; - - $board_id = (int) ($data['board_id'] ?? 0); - if ($board_id > 0) { - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($post_id, '_aksrt_plate_id', $board_id); - } else { - wp_set_object_terms($post_id, [$board_id], 'aksrt_forum_board'); - } - } - - $tags = is_array($data['tags'] ?? null) ? $data['tags'] : []; - if (!empty($tags)) { - $clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5); - wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag'); - } - - $meta_fields = [ - 'visibility' => '_aksrt_visibility', - 'bbs_type' => '_aksrt_bbs_type', - 'special_topic' => '_aksrt_special_topic', - 'cover_type' => '_aksrt_cover_type', - ]; - foreach ($meta_fields as $key => $meta_key) { - if (isset($data[$key])) { - update_post_meta($post_id, $meta_key, sanitize_key((string) $data[$key])); - } - } - - if (isset($data['credits_cost'])) { - $cost = max(0, (int) $data['credits_cost']); - update_post_meta($post_id, '_aksrt_credits_cost', $cost); - update_post_meta($post_id, '_aksrt_pay_credits', $cost); - } - if (isset($data['pay_price'])) { - update_post_meta($post_id, '_aksrt_pay_price', sanitize_text_field((string) $data['pay_price'])); - } - if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1'])); - } - if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2'])); - } - if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') { - $v1 = max(0, (float) $data['pay_vip1_price']); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1); - update_post_meta($post_id, '_aksrt_pay_vip_price', $v1); - } - if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price'])); - } - if (array_key_exists('pay_hide_part', $data)) { - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - } - if (array_key_exists('topic_coupon_s', $data)) { - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0); - } - if (isset($data['visibility_roles']) && is_array($data['visibility_roles'])) { - update_post_meta($post_id, '_aksrt_visibility_roles', array_map('sanitize_key', $data['visibility_roles'])); - } - - $this->forum->save_cover_meta($post_id, $data); - - if (!empty($data['vote_enabled'])) { - $vote_meta = $this->forum->prepare_vote_meta($data); - $this->forum->save_vote_meta($post_id, $vote_meta); - } - - if (isset($data['topic_cat'])) { - $topic_cat = sanitize_text_field((string) $data['topic_cat']); - if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) { - wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat'); - } - } - - return [ - 'post_id' => $post_id, - 'message' => __('草稿已保存', 'aksrt'), - ]; - } - - public function get_user_draft_count(int $user_id): int { - if ($user_id <= 0) { - return 0; - } - global $wpdb; - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_forum_topic' AND post_status = 'draft' AND post_author = %d", - $user_id - )); - } - - /** - * @return array - */ - public function get_user_drafts(int $user_id, int $limit = 20): array { - if ($user_id <= 0) { - return []; - } - - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'draft', - 'author' => $user_id, - 'posts_per_page' => $limit, - 'orderby' => 'modified', - 'order' => 'DESC', - 'no_found_rows' => true, - ]); - - $drafts = []; - while ($query->have_posts()) { - $query->the_post(); - $post = get_post(); - if (!$post) { - continue; - } - - $board_name = ''; - if ($this->forum->is_plate_cpt_active()) { - $plate_id = (int) get_post_meta($post->ID, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $board_name = get_the_title($plate_id); - } - } else { - $terms = wp_get_post_terms($post->ID, 'aksrt_forum_board', ['fields' => 'names']); - if (!is_wp_error($terms) && !empty($terms)) { - $board_name = $terms[0]; - } - } - - $drafts[] = [ - 'id' => $post->ID, - 'title' => $post->post_title ?: __('(无标题)', 'aksrt'), - 'date' => $post->post_modified, - 'board_name' => $board_name, - ]; - } - wp_reset_postdata(); - - return $drafts; - } - - public function delete_draft(int $user_id, int $draft_id): bool { - if ($user_id <= 0 || $draft_id <= 0) { - return false; - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return false; - } - return (bool) wp_delete_post($draft_id, true); - } - - /** - * Load a draft's full data for the compose form. - */ - public function load_draft_data(int $user_id, int $draft_id): ?array { - if ($user_id <= 0 || $draft_id <= 0) { - return null; - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return null; - } - - $board_id = 0; - if ($this->forum->is_plate_cpt_active()) { - $board_id = (int) get_post_meta($draft_id, '_aksrt_plate_id', true); - } else { - $terms = wp_get_post_terms($draft_id, 'aksrt_forum_board', ['fields' => 'ids']); - if (!is_wp_error($terms) && !empty($terms)) { - $board_id = (int) $terms[0]; - } - } - - $tags = wp_get_post_terms($draft_id, 'aksrt_forum_tag', ['fields' => 'names']); - if (is_wp_error($tags)) { - $tags = []; - } - - $topic_cat = ''; - if (taxonomy_exists('aksrt_forum_topic_cat')) { - $cats = wp_get_post_terms($draft_id, 'aksrt_forum_topic_cat', ['fields' => 'names']); - if (!is_wp_error($cats) && !empty($cats)) { - $topic_cat = $cats[0]; - } - } - - $slide_raw = get_post_meta($draft_id, '_aksrt_featured_slide', true); - - return [ - 'draft_id' => $draft_id, - 'title' => $post->post_title, - 'content' => $post->post_content, - 'board_id' => $board_id, - 'tags' => implode(', ', $tags), - 'visibility' => get_post_meta($draft_id, '_aksrt_visibility', true) ?: Forum::VISIBILITY_PUBLIC, - 'bbs_type' => get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD, - 'special_topic' => get_post_meta($draft_id, '_aksrt_special_topic', true) ?: '', - 'cover_type' => get_post_meta($draft_id, '_aksrt_cover_type', true) ?: '', - 'cover_image' => get_post_meta($draft_id, '_aksrt_cover_image', true) ?: '', - 'featured_slide' => is_array($slide_raw) ? $slide_raw : [], - 'featured_video' => get_post_meta($draft_id, '_aksrt_featured_video', true) ?: '', - 'credits_cost' => (int) get_post_meta($draft_id, '_aksrt_credits_cost', true), - 'pay_price' => get_post_meta($draft_id, '_aksrt_pay_price', true) ?: '', - 'credits_vip1' => get_post_meta($draft_id, '_aksrt_credits_vip1', true), - 'credits_vip2' => get_post_meta($draft_id, '_aksrt_credits_vip2', true), - 'pay_vip1_price' => get_post_meta($draft_id, '_aksrt_pay_vip1_price', true), - 'pay_vip2_price' => get_post_meta($draft_id, '_aksrt_pay_vip2_price', true), - 'pay_hide_part' => (int) get_post_meta($draft_id, '_aksrt_pay_hide_part', true) === 1, - 'topic_coupon_s' => (int) get_post_meta($draft_id, '_aksrt_topic_coupon_s', true) === 1, - 'visibility_roles' => get_post_meta($draft_id, '_aksrt_visibility_roles', true) ?: [], - 'topic_cat' => $topic_cat, - 'vote_enabled' => get_post_meta($draft_id, '_aksrt_vote_enabled', true) === '1', - 'vote_title' => get_post_meta($draft_id, '_aksrt_vote_title', true) ?: '', - 'vote_multiple' => get_post_meta($draft_id, '_aksrt_vote_multiple', true) === '1', - 'vote_type' => get_post_meta($draft_id, '_aksrt_vote_type', true) ?: 'standard', - 'vote_expiry_days' => (int) get_post_meta($draft_id, '_aksrt_vote_expiry_days', true), - 'vote_options' => $this->get_vote_options_text($draft_id), - ]; - } - - private function get_vote_options_text(int $post_id): string { - $options = get_post_meta($post_id, '_aksrt_vote_options', true); - if (!is_array($options)) { - return ''; - } - return implode("\n", array_column($options, 'label')); - } - - // ────────────────────────────────────────────────────────── - // AJAX handlers - // ────────────────────────────────────────────────────────── - - public function ajax_save_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $data = [ - 'draft_id' => (int) ($_POST['draft_id'] ?? 0), - 'board_id' => (int) ($_POST['board_id'] ?? 0), - 'title' => sanitize_text_field((string) ($_POST['title'] ?? '')), - 'content' => wp_kses_post((string) ($_POST['content'] ?? '')), - 'tags' => $this->forum->split_tags((string) ($_POST['tags'] ?? '')), - 'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')), - 'visibility' => sanitize_key((string) ($_POST['visibility'] ?? Forum::VISIBILITY_PUBLIC)), - 'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? Forum::BBS_TYPE_STANDARD)), - 'topic_cat' => sanitize_text_field((string) ($_POST['topic_cat'] ?? '')), - 'vote_enabled' => !empty($_POST['vote_enabled']), - 'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')), - 'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')), - 'vote_multiple' => !empty($_POST['vote_multiple']), - 'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')), - 'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0), - 'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')), - 'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')), - 'featured_slide' => (string) ($_POST['featured_slide'] ?? ''), - 'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')), - 'credits_cost' => (int) ($_POST['credits_cost'] ?? 0), - 'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')), - 'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '', - 'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '', - 'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '', - 'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '', - 'pay_hide_part' => !empty($_POST['pay_hide_part']), - 'topic_coupon_s' => !empty($_POST['topic_coupon_s']), - 'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles']) - ? array_map('sanitize_key', $_POST['visibility_roles']) : [], - ]; - - $result = $this->save_draft($user_id, $data); - if (is_wp_error($result)) { - wp_send_json_error(['message' => $result->get_error_message()]); - } - - wp_send_json_success($result); - } - - public function ajax_load_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - $draft_id = (int) ($_GET['draft_id'] ?? $_POST['draft_id'] ?? 0); - - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $data = $this->load_draft_data($user_id, $draft_id); - if (!$data) { - wp_send_json_error(['message' => __('草稿不存在', 'aksrt')]); - } - - wp_send_json_success($data); - } - - public function ajax_delete_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - $draft_id = (int) ($_POST['draft_id'] ?? 0); - - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - if (!$this->delete_draft($user_id, $draft_id)) { - wp_send_json_error(['message' => __('删除失败', 'aksrt')]); - } - - wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]); - } - - // ────────────────────────────────────────────────────────── - // Publish draft (transition from draft → publish/pending) - // ────────────────────────────────────────────────────────── - - public function publish_draft(int $user_id, int $draft_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return new \WP_Error('invalid_draft', __('草稿不存在', 'aksrt')); - } - - $data['draft_id'] = $draft_id; - $save_result = $this->save_draft($user_id, $data); - if (is_wp_error($save_result)) { - return $save_result; - } - - $board_id = (int) ($data['board_id'] ?? 0); - $title = sanitize_text_field((string) ($data['title'] ?? '')); - $content = wp_kses_post((string) ($data['content'] ?? '')); - - if ($board_id <= 0 || $title === '' || $content === '') { - return new \WP_Error('invalid_payload', __('请填写版块、标题和内容', 'aksrt')); - } - - $can_publish = $this->forum->can_user_publish_directly($user_id, $board_id); - $post_status = $can_publish ? 'publish' : 'pending'; - $review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING; - - wp_update_post([ - 'ID' => $draft_id, - 'post_status' => $post_status, - ]); - - update_post_meta($draft_id, '_aksrt_review_status', $review_status); - update_post_meta($draft_id, '_aksrt_sticky', Forum::STICKY_NONE); - update_post_meta($draft_id, '_aksrt_featured', '0'); - update_post_meta($draft_id, '_aksrt_locked', '0'); - update_post_meta($draft_id, '_aksrt_views', 0); - update_post_meta($draft_id, '_aksrt_last_reply', (string) get_post_field('post_date', $draft_id)); - - $bbs_type = get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD; - if ($bbs_type === Forum::BBS_TYPE_QUESTION) { - update_post_meta($draft_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED); - } - - if ($post_status === 'publish') { - $this->forum->sync_board_count($board_id); - if (class_exists('\AKSRT\Credits\Earn')) { - do_action('aksrt_forum_topic_created', $user_id, $draft_id); - } - } - - if ($post_status === 'pending') { - $this->forum->notify_topic_pending($draft_id, $user_id); - } - - return [ - 'post_id' => $draft_id, - 'message' => $post_status === 'publish' - ? __('主题已发布', 'aksrt') - : __('主题已提交审核,审核通过后对外展示', 'aksrt'), - 'redirect' => $post_status === 'publish' - ? get_permalink($draft_id) - : add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()), - ]; - } -} diff --git a/src/AKSRT/Forum/Forum.php b/src/AKSRT/Forum/Forum.php deleted file mode 100644 index 1d9b8fae..00000000 --- a/src/AKSRT/Forum/Forum.php +++ /dev/null @@ -1,959 +0,0 @@ -perm_service) { - $this->perm_service = new PermissionService($this); - } - return $this->perm_service; - } - - private function score_service(): ScoreService { - if (!$this->score_service) { - $this->score_service = new ScoreService($this); - } - return $this->score_service; - } - - private function comment_service(): CommentService { - if (!$this->comment_service) { - $this->comment_service = new CommentService($this); - } - return $this->comment_service; - } - - private function draft_service(): DraftService { - if (!$this->draft_svc) { - // Composer PSR-4: AKSRT\Forum\DraftService → src/AKSRT/Forum/DraftService.php - // Never require legacy class-aksrt-forum-draft-service.php. - if (!class_exists(DraftService::class, true)) { - throw new \RuntimeException( - 'AKSRT\\Forum\\DraftService missing; deploy src/AKSRT/Forum/DraftService.php and run composer dump-autoload if needed' - ); - } - $this->draft_svc = new DraftService($this); - } - return $this->draft_svc; - } - - private ?TopicService $topic_svc = null; - - private ?ModerationService $mod_svc = null; - private function moderation_service(): ModerationService { - if ($this->mod_svc === null) { - $this->mod_svc = new ModerationService($this); - } - return $this->mod_svc; - } - - private ?ViewBuilder $view_builder = null; - private function view_builder(): ViewBuilder { - if ($this->view_builder === null) { - $this->view_builder = new ViewBuilder($this); - } - return $this->view_builder; - } - - private function topic_service(): TopicService { - if ($this->topic_svc === null) { - $this->topic_svc = new TopicService($this); - } - return $this->topic_svc; - } - private function board_service(): BoardService { - if (!$this->board_svc) { - $this->board_svc = new BoardService($this); - } - return $this->board_svc; - } - - public function is_plate_cpt_active(): bool { - // Plate CPT is the native board model; no option migration gate. - return post_type_exists('aksrt_forum_plate'); - } - - public function get_config(): array { - if (class_exists('\AKSRT\Settings\Settings')) { - return \AKSRT\Settings\Settings::get_forum_settings(); - } - - return ['enabled' => false]; - } - - /** - * Alias kept for topic-service helpers that still call get_forum_config(). - */ - public function get_forum_config(): array { - return $this->get_config(); - } - - public function get_boards(array $args = []): array { - return $this->board_service()->get_boards($args); - } - - public function create_board(array $data): int { - return $this->board_service()->create_board($data); - } - - public function update_board(int $board_id, array $data): bool { - return $this->board_service()->update_board($board_id, $data); - } - - public function delete_board(int $board_id): bool { - return $this->board_service()->delete_board($board_id); - } - - public function reorder_boards(array $order_map): bool { - return $this->board_service()->reorder_boards($order_map); - } - - // §7.0 Plate CPT methods —板块 CPT 化 CRUD - - public function get_plates(array $args = []): array { - return $this->board_service()->get_plates($args); - } - - public function get_plate(int $plate_id): ?array { - return $this->board_service()->get_plate($plate_id); - } - - public function build_plate_payload(\WP_Post $post): array { - return $this->board_service()->build_plate_payload($post); - } - - public function create_plate(array $data): int { - return $this->board_service()->create_plate($data); - } - - public function update_plate(int $plate_id, array $data): bool { - return $this->board_service()->update_plate($plate_id, $data); - } - - public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool { - return $this->board_service()->delete_plate($plate_id, $cascade, $move_to); - } - - public function reorder_plates(array $order_map): bool { - return $this->board_service()->reorder_plates($order_map); - } - - public function get_topic_plate_id(int $topic_id): int { - return $this->board_service()->get_topic_plate_id($topic_id); - } - - public function is_plate_moderator(int $user_id, int $plate_id): bool { - return $this->board_service()->is_plate_moderator($user_id, $plate_id); - } - - public function get_plate_view(int $plate_id, array $args = []): array { - return $this->board_service()->get_plate_view($plate_id, $args); - } - - public function get_topics(array $args = []): array { - $view = $this->get_topic_list($args); - return $view['items']; - } - - public function get_topic_list(array $args = []): array { - return $this->view_builder()->get_topic_list($args); - } - - public function get_home_view(int $user_id = 0): array { - return $this->view_builder()->get_home_view($user_id); - } - - public function get_follow_stream(int $user_id, int $page = 1, int $limit = 15): array { - return $this->view_builder()->get_follow_stream($user_id, $page, $limit); - } - - public function get_synthesis_stream(int $page = 1, int $limit = 15, int $viewer = 0): array { - return $this->view_builder()->get_synthesis_stream($page, $limit, $viewer); - } - - public function get_pending_stream(int $user_id, int $page = 1, int $limit = 15): array { - return $this->view_builder()->get_pending_stream($user_id, $page, $limit); - } - - public function ajax_home_tab(): void { - $this->view_builder()->ajax_home_tab(); - } - - public function ajax_board_topics(): void { - $this->view_builder()->ajax_board_topics(); - } - - public function register_widget_routes(): void { - register_rest_route('aksrt/v1', '/forum/frontend/widgets/topics', [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_widget_topics'], - 'permission_callback' => '__return_true', - ]); - register_rest_route('aksrt/v1', '/forum/frontend/widgets/plates', [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_widget_plates'], - 'permission_callback' => '__return_true', - ]); - } - - public function rest_widget_topics(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { - return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500); - } - $instance = [ - 'count' => max(1, min(30, (int) ($request->get_param('count') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')), - 'plate_id' => max(0, (int) ($request->get_param('plate_id') ?? 0)), - 'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), - ]; - $page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1)); - $payload = \AKSRT\Widgets\WidgetForumTopicLists::query_payload($instance, $page); - return new \WP_REST_Response([ - 'success' => true, - 'html' => (string) ($payload['html'] ?? ''), - 'paginate_html' => (string) ($payload['paginate_html'] ?? ''), - 'has_more' => !empty($payload['has_more']), - 'page' => (int) ($payload['page'] ?? $page), - 'max_pages' => (int) ($payload['max_pages'] ?? 1), - 'total' => (int) ($payload['total'] ?? 0), - ], 200); - } - - public function rest_widget_plates(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) { - return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500); - } - $instance = [ - 'showposts' => max(1, min(30, (int) ($request->get_param('showposts') ?? $request->get_param('count') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')), - 'filter' => sanitize_key((string) ($request->get_param('filter') ?? '')), - 'cat' => sanitize_text_field((string) ($request->get_param('cat') ?? '')), - ]; - $payload = \AKSRT\Widgets\WidgetForumPlateLists::query_payload($instance); - return new \WP_REST_Response([ - 'success' => true, - 'html' => (string) ($payload['html'] ?? ''), - ], 200); - } - - - - public function render_board_cards(array $boards): void { - $this->view_builder()->render_board_cards($boards); - } - - public function render_topic_row(array $topic, string $style = 'detail'): void { - $this->view_builder()->render_topic_row($topic, $style); - } - - public function get_board_view(int $board_id, array $args = []): array { - return $this->board_service()->get_board_view($board_id, $args); - } - - public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool { - return $this->board_service()->can_view_plate($user_id, $plate_id, $allow_view); - } - - public function can_compose_in_plate(int $user_id, int $plate_id): bool { - return $this->board_service()->can_compose_in_plate($user_id, $plate_id); - } - - public function is_user_following_plate(int $user_id, int $plate_id): bool { - return $this->board_service()->is_user_following_plate($user_id, $plate_id); - } - - public function is_plate_follow_pay_enabled(int $plate_id): bool { - return $this->board_service()->is_plate_follow_pay_enabled($plate_id); - } - - public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array { - return $this->board_service()->resolve_plate_follow_pay_price($plate_id, $user_id); - } - - public function mark_plate_followed(int $user_id, int $plate_id): void { - $this->board_service()->mark_plate_followed($user_id, $plate_id); - } - - public function toggle_follow_plate(int $user_id, int $plate_id): array { - return $this->board_service()->toggle_follow_plate($user_id, $plate_id); - } - - public function get_user_followed_plates(int $user_id): array { - return $this->board_service()->get_user_followed_plates($user_id); - } - - public function get_user_managed_plates(int $user_id): array { - return $this->board_service()->get_user_managed_plates($user_id); - } - - public function get_user_forum_topics(int $user_id, array $args = []): array { - return $this->view_builder()->get_user_forum_topics($user_id, $args); - } - - public function get_user_favorite_topics(int $user_id, int $page = 1, int $limit = 10): array { - return $this->view_builder()->get_user_favorite_topics($user_id, $page, $limit); - } - - public function get_user_forum_stats(int $user_id): array { - return $this->view_builder()->get_user_forum_stats($user_id); - } - - public function increment_plate_views(int $plate_id): void { - $this->board_service()->increment_plate_views($plate_id); - } - - public function is_hot_plate(int $plate_id): bool { - return $this->board_service()->is_hot_plate($plate_id); - } - - public function get_plate_categories(): array { - return $this->board_service()->get_plate_categories(); - } - - public function is_cat_moderator(int $user_id, int $cat_id): bool { - return $this->board_service()->is_cat_moderator($user_id, $cat_id); - } - - public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array { - return $this->board_service()->get_sorted_plates($orderby, $order, $limit); - } - - public function get_topic_view(int $topic_id, int $user_id = 0): array { - return $this->view_builder()->get_topic_view($topic_id, $user_id); - } - - public function get_compose_view(int $user_id, array $args = []): array { - return $this->view_builder()->get_compose_view($user_id, $args); - } - - public function get_stats(): array { - return $this->view_builder()->get_stats(); - } - - public function can_edit_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->can_edit_topic($user_id, $topic_id); - } - - public function can_moderate_topic(int $user_id, int $topic_id): bool { - return $this->forum_user_can($user_id, 'topic_moderate', $topic_id); - } - - public function update_topic(int $topic_id, array $data) { - return $this->topic_service()->update_topic($topic_id, $data); - } - - public function get_tags(): array { - return $this->view_builder()->get_tags(); - } - - public function get_tag_archive_view(array $args = []): array { - return $this->view_builder()->get_tag_archive_view($args); - } - - public function init(): void { - add_action('wp_ajax_aksrt_forum_update_topic', [$this, 'ajax_update_topic']); - add_action('wp_ajax_aksrt_forum_topic_list', [$this, 'ajax_topic_list']); - add_action('wp_ajax_aksrt_forum_create_topic', [$this, 'ajax_create_topic']); - add_action('wp_ajax_aksrt_forum_follow_plate', [$this, 'ajax_follow_plate']); - add_action('wp_ajax_aksrt_forum_purchase_plate_follow', [$this, 'ajax_purchase_plate_follow']); - add_action('wp_ajax_aksrt_forum_unlock_topic', [$this, 'ajax_unlock_topic']); - - // §7.4 draft persistence - add_action('wp_ajax_aksrt_forum_save_draft', [$this, 'ajax_save_draft']); - add_action('wp_ajax_aksrt_forum_load_draft', [$this, 'ajax_load_draft']); - add_action('wp_ajax_aksrt_forum_delete_draft', [$this, 'ajax_delete_draft']); - - // §7.4 remote search selectors - add_action('wp_ajax_aksrt_forum_search_boards', [$this, 'ajax_search_boards']); - add_action('wp_ajax_nopriv_aksrt_forum_search_boards', [$this, 'ajax_search_boards']); - add_action('wp_ajax_aksrt_forum_search_tags', [$this, 'ajax_search_tags']); - add_action('wp_ajax_nopriv_aksrt_forum_search_tags', [$this, 'ajax_search_tags']); - add_action('wp_ajax_aksrt_forum_search_topics', [$this, 'ajax_search_topics']); - add_action('wp_ajax_nopriv_aksrt_forum_search_topics', [$this, 'ajax_search_topics']); - - // §7.4 AJAX-based topic editing - add_action('wp_ajax_aksrt_forum_edit_topic', [$this, 'ajax_edit_topic']); - - // §7.6 forum comment engine - add_action('wp_ajax_aksrt_forum_comments', [$this, 'ajax_forum_comments']); - add_action('wp_ajax_nopriv_aksrt_forum_comments', [$this, 'ajax_forum_comments']); - add_action('wp_ajax_aksrt_forum_comment_top', [$this, 'ajax_comment_topping']); - - self::register_hide_shortcode(); - - // §7.10 forum home tab AJAX - add_action('wp_ajax_aksrt_forum_home_tab', [$this, 'ajax_home_tab']); - add_action('wp_ajax_nopriv_aksrt_forum_home_tab', [$this, 'ajax_home_tab']); - - // Public plate/board topic list (sort tabs) - add_action('wp_ajax_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); - add_action('wp_ajax_nopriv_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); - add_action('rest_api_init', [$this, 'register_widget_routes']); - - add_action('admin_init', [$this, 'maybe_migrate_boards'], 25); - add_filter('template_include', [$this, 'template_routing']); - add_action('wp', [$this, 'maybe_refresh_recommend_on_view']); - add_action('comment_post', [$this, 'on_forum_comment_posted'], 10, 3); - - // §7.2 板块多维统计 hooks - add_action('save_post_aksrt_forum_topic', [$this, 'on_topic_saved_sync_plate'], 20, 2); - add_action('comment_post', [$this, 'on_comment_sync_plate_reply'], 25, 3); - - add_action('wp_login', [$this, 'cap_cache_on_login'], 10, 2); - add_action('updated_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('added_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('deleted_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('updated_term_meta', [$this, 'cap_cache_on_cat_meta'], 10, 3); - add_action('save_post_aksrt_forum_plate', [$this, 'cap_cache_on_plate_save'], 10, 1); - } - - public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void { - $this->moderation_service()->on_topic_saved_sync_plate($post_id, $post); - } - - public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void { - $this->moderation_service()->on_comment_sync_plate_reply($comment_id, $approved, $commentdata); - } - - public function template_routing(string $template): string { - return $this->view_builder()->template_routing($template); - } - - public function update_topic_meta(int $topic_id, array $meta): bool { - return $this->topic_service()->update_topic_meta($topic_id, $meta); - } - - public function is_moderator(int $user_id, int $topic_id): bool { - return $this->moderation_service()->is_moderator($user_id, $topic_id); - } - - public function ajax_update_topic(): void { - $this->topic_service()->ajax_update_topic(); - } - - public function ajax_topic_list(): void { - $this->view_builder()->ajax_topic_list(); - } - - public function ajax_create_topic(): void { - $this->topic_service()->ajax_create_topic(); - } - - public function create_topic(int $user_id, array $data) { - return $this->topic_service()->create_topic($user_id, $data); - } - - public function moderate_topic(int $topic_id, string $action, array $payload, int $operator_id = 0) { - return $this->topic_service()->moderate_topic($topic_id, $action, $payload, $operator_id); - } - - public function submit_vote(int $topic_id, int $user_id, array $option_ids) { - return $this->topic_service()->submit_vote($topic_id, $user_id, $option_ids); - } - - public function ajax_follow_plate(): void { - $this->board_service()->ajax_follow_plate(); - } - - public function ajax_purchase_plate_follow(): void { - $this->board_service()->ajax_purchase_plate_follow(); - } - - public function ajax_unlock_topic(): void { - $this->topic_service()->ajax_unlock_topic(); - } - - /** - * Lightweight result helper for topic service actions. - * - * @return array{success:bool,message:string} - */ - public function success(string $message = '', array $extra = []): array { - return array_merge(['success' => true, 'message' => $message], $extra); - } - - /** - * @return array{success:bool,message:string} - */ - public function error(string $message = '', array $extra = []): array { - return array_merge(['success' => false, 'message' => $message], $extra); - } - - public function apply_moderator(int $user_id, int $board_id, string $reason) { - return $this->moderation_service()->apply_moderator($user_id, $board_id, $reason); - } - - public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) { - return $this->moderation_service()->review_moderator_application($application_id, $status, $note, $reviewer_id); - } - - public function get_moderator_application_list(array $args = []): array { - return $this->moderation_service()->get_moderator_application_list($args); - } - - public function sync_board_count(int $board_id): void { - $this->moderation_service()->sync_board_count($board_id); - } - - public function sync_plate_counts(int $plate_id): void { - $this->moderation_service()->sync_plate_counts($plate_id); - } - - public function get_board_moderators(int $board_id): array { - return $this->moderation_service()->get_board_moderators($board_id); - } - - public function get_plate_moderators(int $plate_id): array { - return $this->moderation_service()->get_plate_moderators($plate_id); - } - - public function is_forum_admin(int $user_id): bool { - return $this->moderation_service()->is_forum_admin($user_id); - } - - public function is_board_moderator(int $user_id, int $board_id): bool { - return $this->moderation_service()->is_board_moderator($user_id, $board_id); - } - - public function get_board_moderator_profiles(int $board_id): array { - return $this->moderation_service()->get_board_moderator_profiles($board_id); - } - - public function get_board_moderator_profiles_with_roles(int $board_id): array { - return $this->moderation_service()->get_board_moderator_profiles_with_roles($board_id); - } - - public function get_moderator_roles(int $board_id): array { - return $this->moderation_service()->get_moderator_roles($board_id); - } - - public function set_moderator_role(int $user_id, int $board_id, string $role): bool { - return $this->moderation_service()->set_moderator_role($user_id, $board_id, $role); - } - - public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool { - return $this->moderation_service()->add_moderator($user_id, $board_id, $role); - } - - public function remove_moderator(int $user_id, int $board_id): bool { - return $this->moderation_service()->remove_moderator($user_id, $board_id); - } - - public function get_user_moderator_boards(int $user_id): array { - return $this->moderation_service()->get_user_moderator_boards($user_id); - } - - public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool { - return $this->moderation_service()->has_moderator_role($user_id, $board_id, $min_role); - } - - public function get_board_moderator_applications(int $board_id): array { - return $this->moderation_service()->get_board_moderator_applications($board_id); - } - - public function get_plate_moderator_applications(int $plate_id): array { - return $this->moderation_service()->get_plate_moderator_applications($plate_id); - } - - public function get_board_application_for_user(int $user_id, int $board_id): ?array { - return $this->moderation_service()->get_board_application_for_user($user_id, $board_id); - } - - public function get_visibility_options(): array { - return $this->topic_service()->get_visibility_options(); - } - - public function get_review_options(): array { - return $this->topic_service()->get_review_options(); - } - - public function can_user_publish_directly(int $user_id, int $board_id = 0): bool { - return $this->topic_service()->can_user_publish_directly($user_id, $board_id); - } - - public function get_sticky_topics_for_board(int $board_id): array { - return $this->view_builder()->get_sticky_topics_for_board($board_id); - } - - public function get_topic_detail(int $topic_id, int $viewer_user_id = 0): array { - return $this->view_builder()->get_topic_detail($topic_id, $viewer_user_id); - } - - public function increment_view_count(int $topic_id): void { - $this->view_builder()->increment_view_count($topic_id); - } - - public function maybe_migrate_boards(): void { - $this->board_service()->maybe_migrate_boards(); - } - - public function build_topic_payload(\WP_Post $post, int $viewer_user_id = 0): array { - return $this->topic_service()->build_topic_payload($post, $viewer_user_id); - } - - public function unlock_topic_with_credits(int $user_id, int $topic_id): array { - return $this->topic_service()->unlock_topic_with_credits($user_id, $topic_id); - } - - public function has_user_unlocked_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->has_user_unlocked_topic($user_id, $topic_id); - } - - public function has_user_paid_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->has_user_paid_topic($user_id, $topic_id); - } - - public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array { - return $this->topic_service()->resolve_topic_pay_price($topic_id, $user_id); - } - - public function mark_topic_unlocked(int $user_id, int $topic_id): void { - $this->topic_service()->mark_topic_unlocked($user_id, $topic_id); - } - - public function prepare_vote_meta(array $data): array { - return $this->topic_service()->prepare_vote_meta($data); - } - - public function save_vote_meta(int $topic_id, array $vote_meta): void { - $this->topic_service()->save_vote_meta($topic_id, $vote_meta); - } - - public function normalize_post_status_query(string $status) { - return $this->topic_service()->normalize_post_status_query($status); - } - - private function build_user_summary(int $user_id): array { - return $this->topic_service()->build_user_summary($user_id); - } - - private function get_user_avatar_url(int $user_id): string { - return $this->topic_service()->get_user_avatar_url($user_id); - } - - public function save_cover_meta(int $post_id, array $data): void { - $this->topic_service()->save_cover_meta($post_id, $data); - } - - public function split_tags(string $tags): array { - return $this->topic_service()->split_tags($tags); - } - - private function get_visibility_map(): array { - return $this->topic_service()->get_visibility_map(); - } - - private function get_review_map(): array { - return $this->topic_service()->get_review_map(); - } - - private function get_visibility_label(string $visibility): string { - return $this->topic_service()->get_visibility_label($visibility); - } - - private function get_review_label(string $review_status): string { - return $this->topic_service()->get_review_label($review_status); - } - - public function is_favorited(int $user_id, int $topic_id): bool { - return $this->score_service()->is_favorited($user_id, $topic_id); - } - - public function toggle_favorite(int $user_id, int $topic_id) { - return $this->score_service()->toggle_favorite($user_id, $topic_id); - } - - public function score_topic(int $user_id, int $topic_id, int $amount) { - return $this->score_service()->score_topic($user_id, $topic_id, $amount); - } - - public function get_score_detail(int $topic_id): array { - return $this->score_service()->get_score_detail($topic_id); - } - - public function calculate_recommend_score(int $topic_id): float { - return $this->score_service()->calculate_recommend_score($topic_id); - } - - public function refresh_recommend_score(int $topic_id): float { - return $this->score_service()->refresh_recommend_score($topic_id); - } - - public function batch_refresh_recommend_scores(int $limit = 100): int { - return $this->score_service()->batch_refresh_recommend_scores($limit); - } - - public function maybe_refresh_recommend_on_view(): void { - $this->score_service()->maybe_refresh_recommend_on_view(); - } - - public function notify(int $user_id, string $type, string $content, array $data = []): void { - $this->score_service()->notify($user_id, $type, $content, $data); - } - - public function notify_topic_pending(int $topic_id, int $author_id): void { - $this->score_service()->notify_topic_pending($topic_id, $author_id); - } - - public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void { - $this->score_service()->notify_moderation_result($topic_id, $action, $operator_id); - } - - public function notify_topic_reply(int $topic_id, int $commenter_id): void { - $this->score_service()->notify_topic_reply($topic_id, $commenter_id); - } - - public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void { - $this->score_service()->notify_topic_scored($topic_id, $scorer_id, $amount); - } - - const COMMENT_TOP_NONE = 0; - const COMMENT_TOP_NORMAL = 1; - const COMMENT_TOP_HOT = 2; - const COMMENT_TOP_BEST = 3; - - public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void { - $this->score_service()->on_forum_comment_posted($comment_id, $approved, $commentdata); - } - - public function ajax_forum_comments(): void { - $this->comment_service()->ajax_forum_comments(); - } - - public function ajax_comment_topping(): void { - $this->comment_service()->ajax_comment_topping(); - } - - public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void { - $this->comment_service()->render_forum_comments($comments, $topic_id, $adopted_id); - } - - public function adopt_answer(int $topic_id, int $comment_id, int $operator_id) { - return $this->comment_service()->adopt_answer($topic_id, $comment_id, $operator_id); - } - - public function get_adopted_comment_id(int $topic_id): int { - return $this->comment_service()->get_adopted_comment_id($topic_id); - } - - public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array { - return $this->comment_service()->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page); - } - - public function set_comment_topping(int $comment_id, int $level): bool { - return $this->comment_service()->set_comment_topping($comment_id, $level); - } - - public function maybe_mark_hot_comment(int $comment_id): bool { - return $this->comment_service()->maybe_mark_hot_comment($comment_id); - } - - public function is_hot_comment(int $comment_id): bool { - return $this->comment_service()->is_hot_comment($comment_id); - } - - public function get_commenter_badge(int $user_id, int $topic_id): ?array { - return $this->comment_service()->get_commenter_badge($user_id, $topic_id); - } - - public function save_draft(int $user_id, array $data) { - return $this->draft_service()->save_draft($user_id, $data); - } - - public function get_user_draft_count(int $user_id): int { - return $this->draft_service()->get_user_draft_count($user_id); - } - - public function get_user_drafts(int $user_id, int $limit = 20): array { - return $this->draft_service()->get_user_drafts($user_id, $limit); - } - - public function delete_draft(int $user_id, int $draft_id): bool { - return $this->draft_service()->delete_draft($user_id, $draft_id); - } - - public function load_draft_data(int $user_id, int $draft_id): ?array { - return $this->draft_service()->load_draft_data($user_id, $draft_id); - } - - public function ajax_save_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_save_draft(); - }); - } - - public function ajax_load_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_load_draft(); - }); - } - - public function ajax_delete_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_delete_draft(); - }); - } - - /** - * Draft AJAX must never leak WP recovery HTML — return JSON on Throwable. - */ - private function run_draft_ajax(callable $callback): void { - try { - $callback($this); - } catch (\Throwable $e) { - if (defined('WP_DEBUG') && WP_DEBUG) { - error_log('[AKSRT forum draft ajax] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); - } - // Prefer JSON even when autoload/require still broken on a partial deploy. - if (!headers_sent()) { - status_header(500); - } - wp_send_json_error([ - 'message' => sprintf( - /* translators: %s: exception message */ - __('草稿操作失败:%s', 'aksrt'), - $e->getMessage() - ), - 'file' => basename($e->getFile()), - 'line' => $e->getLine(), - ], 500); - } - } - - public function ajax_search_boards(): void { - $this->view_builder()->ajax_search_boards(); - } - - public function ajax_search_tags(): void { - $this->view_builder()->ajax_search_tags(); - } - - public function ajax_search_topics(): void { - $this->view_builder()->ajax_search_topics(); - } - - public function ajax_edit_topic(): void { - $this->topic_service()->ajax_edit_topic(); - } - - public function get_compose_capabilities(int $user_id): array { - return $this->permission_service()->get_compose_capabilities($user_id); - } - - public function sanitize_topic_content(int $user_id, string $content): string { - return $this->topic_service()->sanitize_topic_content($user_id, $content); - } - - public static function register_hide_shortcode(): void { - add_shortcode('aksrt_hide', [TopicService::class, 'render_hide_shortcode']); - } - - public function publish_draft(int $user_id, int $draft_id, array $data) { - return $this->draft_service()->publish_draft($user_id, $draft_id, $data); - } - - // - // 四级层级(对标 zibll): - // TIER_FORUM_ADMIN (4) — bbs_admin_users + manage_options - // TIER_CAT_MOD (3) — aksrt_plate_cat term meta 'moderator' - // TIER_PLATE_AUTHOR(2) — plate CPT post_author(超级版主) - // TIER_MODERATOR (1) — plate CPT post meta '_aksrt_moderators' list - // - // 公开 API: - // forum_user_can( $uid, $cap, $context_id = 0 ): bool - // get_user_plate_tier( $uid, $plate_id ): int - // flush_cap_cache( $uid = 0 ): void - - public function get_user_plate_tier(int $user_id, int $plate_id): int { - return $this->permission_service()->get_user_plate_tier($user_id, $plate_id); - } - - public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool { - return $this->permission_service()->forum_user_can($user_id, $cap, $context_id); - } - - public function flush_cap_cache(int $user_id = 0): void { - $this->permission_service()->flush_cap_cache($user_id); - } - - public function cap_cache_on_login(string $user_login, \WP_User $user): void { - $this->permission_service()->cap_cache_on_login($user_login, $user); - } - - public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $this->permission_service()->cap_cache_on_plate_meta($meta_id, $object_id, $meta_key); - } - - public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $this->permission_service()->cap_cache_on_cat_meta($meta_id, $object_id, $meta_key); - } - - public function cap_cache_on_plate_save($post_id): void { - $this->permission_service()->cap_cache_on_plate_save($post_id); - } -} diff --git a/src/AKSRT/Forum/ModerationService.php b/src/AKSRT/Forum/ModerationService.php deleted file mode 100644 index d8ac9713..00000000 --- a/src/AKSRT/Forum/ModerationService.php +++ /dev/null @@ -1,717 +0,0 @@ -forum = $forum; - } - - public function apply_moderator(int $user_id, int $board_id, string $reason) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($board_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return new \WP_Error('invalid_board', __('版块不存在', 'aksrt')); - } - } else { - $board = get_term($board_id, 'aksrt_forum_board'); - if (!$board || is_wp_error($board)) { - return new \WP_Error('invalid_board', __('版块不存在', 'aksrt')); - } - } - - if ($this->is_board_moderator($user_id, $board_id)) { - return new \WP_Error('already_moderator', __('你已经是该版块版主', 'aksrt')); - } - - $applications = $this->get_board_moderator_applications($board_id); - foreach ($applications as $application) { - if ((int) ($application['user_id'] ?? 0) === $user_id && (string) ($application['status'] ?? '') === Forum::APPLICATION_PENDING) { - return new \WP_Error('application_exists', __('你已经提交过申请,等待处理即可', 'aksrt')); - } - } - - $reason = trim($reason); - if ($reason === '') { - return new \WP_Error('empty_reason', __('请填写申请理由', 'aksrt')); - } - - $application = [ - 'id' => wp_generate_uuid4(), - 'board_id' => $board_id, - 'user_id' => $user_id, - 'reason' => sanitize_textarea_field($reason), - 'status' => Forum::APPLICATION_PENDING, - 'created_at' => current_time('mysql'), - 'reviewed_at' => '', - 'review_note' => '', - 'reviewer_id' => 0, - ]; - - $applications[] = $application; - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($board_id, '_aksrt_moderator_applications', $applications); - } else { - update_term_meta($board_id, '_aksrt_moderator_applications', $applications); - } - - return [ - 'message' => __('版主申请已提交', 'aksrt'), - 'application' => $application, - ]; - } - - /** - * Review a moderator application. - * - * @return array|\WP_Error - */ - public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) { - if ($reviewer_id <= 0 || !$this->forum->forum_user_can($reviewer_id, 'moderator_review')) { - return new \WP_Error('forbidden', __('权限不足', 'aksrt')); - } - - if (!in_array($status, [Forum::APPLICATION_APPROVED, Forum::APPLICATION_REJECTED], true)) { - return new \WP_Error('invalid_status', __('申请处理状态无效', 'aksrt')); - } - - $use_cpt = $this->forum->is_plate_cpt_active(); - $boards = $this->forum->get_boards(); - $flat_boards = $this->flatten_boards($boards); - - foreach ($flat_boards as $board) { - $board_id = (int) ($board['id'] ?? 0); - $applications = $this->get_board_moderator_applications($board_id); - $updated = false; - - foreach ($applications as &$application) { - if ((string) ($application['id'] ?? '') !== $application_id) { - continue; - } - - $application['status'] = $status; - $application['review_note'] = sanitize_textarea_field($note); - $application['reviewed_at'] = current_time('mysql'); - $application['reviewer_id'] = $reviewer_id; - $updated = true; - - if ($status === Forum::APPLICATION_APPROVED) { - $this->add_moderator((int) ($application['user_id'] ?? 0), $board_id); - } - - break; - } - unset($application); - - if ($updated) { - if ($use_cpt) { - update_post_meta($board_id, '_aksrt_moderator_applications', $applications); - } else { - update_term_meta($board_id, '_aksrt_moderator_applications', $applications); - } - - $reviewed_application = $this->find_application_by_id($application_id); - if ($reviewed_application) { - $reviewed_user_id = (int) ($reviewed_application['user_id'] ?? 0); - if ($reviewed_user_id > 0 && $reviewed_user_id !== $reviewer_id) { - $board_name = (string) ($board['name'] ?? __('当前版块', 'aksrt')); - $status_label = $status === Forum::APPLICATION_APPROVED - ? __('已通过', 'aksrt') - : __('已拒绝', 'aksrt'); - $payload = [ - 'title' => __('版主申请结果', 'aksrt'), - 'target_url' => isset($board['url']) ? (string) $board['url'] : aksrt_get_forum_home_url(), - ]; - $clean_note = sanitize_textarea_field($note); - if ($clean_note !== '') { - $payload['reason'] = $clean_note; - } - $this->forum->notify( - $reviewed_user_id, - 'system', - sprintf(__('你在「%1$s」提交的版主申请%2$s', 'aksrt'), $board_name, $status_label), - $payload - ); - } - } - - return [ - 'message' => __('申请状态已更新', 'aksrt'), - 'application' => $reviewed_application, - ]; - } - } - - return new \WP_Error('application_not_found', __('申请记录不存在', 'aksrt')); - } - - /** - * Flatten hierarchical boards array (collapse children into flat list). - */ - private function flatten_boards(array $boards): array { - $flat = []; - foreach ($boards as $board) { - $children = $board['children'] ?? []; - unset($board['children']); - $flat[] = $board; - if (!empty($children)) { - $flat = array_merge($flat, $this->flatten_boards($children)); - } - } - return $flat; - } - - /** - * Get flattened moderator application list. - */ - public function get_moderator_application_list(array $args = []): array { - $status = sanitize_key((string) ($args['status'] ?? '')); - $board_id = (int) ($args['board_id'] ?? 0); - $keyword = sanitize_text_field((string) ($args['keyword'] ?? '')); - $items = []; - - foreach ($this->forum->get_boards() as $board) { - $current_board_id = (int) ($board['id'] ?? 0); - if ($board_id > 0 && $current_board_id !== $board_id) { - continue; - } - - foreach ($this->get_board_moderator_applications($current_board_id) as $application) { - if ($status !== '' && (string) ($application['status'] ?? '') !== $status) { - continue; - } - - $user = get_userdata((int) ($application['user_id'] ?? 0)); - $row = [ - 'id' => (string) ($application['id'] ?? ''), - 'board_id' => $current_board_id, - 'board_name' => (string) ($board['name'] ?? ''), - 'user_id' => (int) ($application['user_id'] ?? 0), - 'display_name' => $user ? $user->display_name : '', - 'user_email' => $user ? $user->user_email : '', - 'avatar_url' => $user ? $this->forum->get_user_avatar_url((int) $user->ID) : '', - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? ''), - 'status_label' => $this->get_application_status_label((string) ($application['status'] ?? '')), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer' => $this->forum->build_user_summary((int) ($application['reviewer_id'] ?? 0)), - ]; - - if ($keyword !== '') { - $haystack = strtolower(implode(' ', [ - (string) $row['board_name'], - (string) $row['display_name'], - (string) $row['user_email'], - (string) $row['reason'], - ])); - if (strpos($haystack, strtolower($keyword)) === false) { - continue; - } - } - - $items[] = $row; - } - } - - usort($items, static function ($left, $right): int { - return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); - }); - - return $items; - } - - /** - * Sync board/plate topic count + reply count. - */ - public function sync_board_count(int $board_id): void { - if ($board_id <= 0) { - return; - } - - if ($this->forum->is_plate_cpt_active()) { - $this->sync_plate_counts($board_id); - return; - } - - $count = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'posts_per_page' => 1, - 'fields' => 'ids', - 'tax_query' => [[ - 'taxonomy' => 'aksrt_forum_board', - 'field' => 'term_id', - 'terms' => $board_id, - ]], - 'post_status' => 'publish', - ]); - - update_term_meta($board_id, '_aksrt_topic_count', (int) $count->found_posts); - } - - /** - * Sync plate multi-dimensional counters (§7.2 去规范化统计). - */ - public function sync_plate_counts(int $plate_id): void { - global $wpdb; - - $posts_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->postmeta} pm - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'", - (string) $plate_id - )); - - $reply_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1'", - (string) $plate_id - )); - - $today_start = current_time('Y-m-d') . ' 00:00:00'; - $today_reply_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1' AND c.comment_date >= %s", - (string) $plate_id, - $today_start - )); - - $last_post = $wpdb->get_var($wpdb->prepare( - "SELECT p.post_date FROM {$wpdb->posts} p - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish' - ORDER BY p.post_date DESC LIMIT 1", - (string) $plate_id - )); - - $last_reply = $wpdb->get_var($wpdb->prepare( - "SELECT c.comment_date FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1' - ORDER BY c.comment_date DESC LIMIT 1", - (string) $plate_id - )); - - update_post_meta($plate_id, '_aksrt_posts_count', $posts_count); - update_post_meta($plate_id, '_aksrt_reply_count', $reply_count); - update_post_meta($plate_id, '_aksrt_today_reply_count', $today_reply_count); - update_post_meta($plate_id, '_aksrt_last_post', (string) ($last_post ?: '')); - update_post_meta($plate_id, '_aksrt_last_reply', (string) ($last_reply ?: '')); - } - - /** - * Get board moderator IDs (统一入口,自动检测 CPT/taxonomy). - */ - public function get_board_moderators(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - return $this->get_plate_moderators($board_id); - } - - // Fallback: taxonomy - $moderators = get_term_meta($board_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return []; - } - - return array_values(array_unique(array_filter(array_map('intval', $moderators)))); - } - - /** - * Get plate moderators from CPT meta. - */ - public function get_plate_moderators(int $plate_id): array { - if (!post_type_exists('aksrt_forum_plate')) { - return []; - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return []; - } - - return array_values(array_unique(array_filter(array_map('intval', $moderators)))); - } - - /** - * Check if a user is a forum-level admin (bbs_admin_users setting). - * - * Forum admins have cross-board governance privileges and bypass bans. - */ - public function is_forum_admin(int $user_id): bool { - if ($user_id <= 0) { - return false; - } - - if (user_can($user_id, 'manage_options')) { - return true; - } - - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $admin_users = $forum['admin_users'] ?? []; - - return in_array($user_id, $admin_users, true); - } - - /** - * Check if a user is a board moderator. - */ - public function is_board_moderator(int $user_id, int $board_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_forum_admin($user_id)) { - return true; - } - - return in_array($user_id, $this->get_board_moderators($board_id), true); - } - - /** - * Get board moderator profile rows. - */ - public function get_board_moderator_profiles(int $board_id): array { - $profiles = []; - foreach ($this->get_board_moderators($board_id) as $user_id) { - $user = get_userdata($user_id); - if (!$user) { - continue; - } - - $profiles[] = [ - 'id' => $user_id, - 'display_name' => $user->display_name, - 'user_email' => $user->user_email, - 'avatar_url' => $this->forum->get_user_avatar_url($user_id), - ]; - } - - return $profiles; - } - - /** - * Get board moderator profiles with role information. - */ - public function get_board_moderator_profiles_with_roles(int $board_id): array { - $roles_map = $this->get_moderator_roles($board_id); - $profiles = []; - foreach ($this->get_board_moderators($board_id) as $user_id) { - $user = get_userdata($user_id); - if (!$user) continue; - $profiles[] = [ - 'id' => $user_id, - 'display_name' => $user->display_name, - 'user_email' => $user->user_email, - 'avatar_url' => $this->forum->get_user_avatar_url($user_id), - 'role' => $roles_map[$user_id] ?? 'moderator', - ]; - } - return $profiles; - } - - /** - * Get moderator role map for a board: [user_id => role]. - * - * In CPT mode, also maps plate author → 'plate_author' (超级版主). - */ - public function get_moderator_roles(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - $roles = get_post_meta($board_id, '_aksrt_moderator_roles', true); - $roles = is_array($roles) ? $roles : []; - $post = get_post($board_id); - if ($post && $post->post_author > 0) { - $roles[(int) $post->post_author] = 'plate_author'; - } - return $roles; - } - $roles = get_term_meta($board_id, '_aksrt_moderator_roles', true); - return is_array($roles) ? $roles : []; - } - - /** - * Set a moderator's role on a board. - */ - public function set_moderator_role(int $user_id, int $board_id, string $role): bool { - $allowed = ['super_moderator', 'moderator', 'junior_moderator', 'plate_author', 'cat_moderator']; - if (!in_array($role, $allowed, true)) { - return false; - } - if (!in_array($user_id, $this->get_board_moderators($board_id), true)) { - return false; - } - $roles = $this->get_moderator_roles($board_id); - $roles[$user_id] = $role; - if ($this->forum->is_plate_cpt_active()) { - return (bool) update_post_meta($board_id, '_aksrt_moderator_roles', $roles); - } - return (bool) update_term_meta($board_id, '_aksrt_moderator_roles', $roles); - } - - /** - * Add a moderator to a board with optional role. - */ - public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool { - $current = $this->get_board_moderators($board_id); - if (in_array($user_id, $current, true)) { - $this->set_moderator_role($user_id, $board_id, $role); - return true; - } - $current[] = $user_id; - $new_list = array_values(array_unique(array_map('intval', $current))); - if ($this->forum->is_plate_cpt_active()) { - $result = update_post_meta($board_id, '_aksrt_moderators', $new_list); - } else { - $result = update_term_meta($board_id, '_aksrt_moderators', $new_list); - } - if ($result !== false) { - $this->set_moderator_role($user_id, $board_id, $role); - } - return $result !== false; - } - - /** - * Remove a moderator from a board. - */ - public function remove_moderator(int $user_id, int $board_id): bool { - $current = $this->get_board_moderators($board_id); - $current = array_values(array_diff($current, [$user_id])); - $roles = $this->get_moderator_roles($board_id); - unset($roles[$user_id]); - if ($this->forum->is_plate_cpt_active()) { - $result = update_post_meta($board_id, '_aksrt_moderators', $current); - update_post_meta($board_id, '_aksrt_moderator_roles', $roles); - } else { - $result = update_term_meta($board_id, '_aksrt_moderators', $current); - update_term_meta($board_id, '_aksrt_moderator_roles', $roles); - } - return $result !== false; - } - - /** - * Get all boards a user moderates (with roles). - */ - public function get_user_moderator_boards(int $user_id): array { - $boards = []; - foreach ($this->forum->get_boards() as $board) { - $board_id = (int) ($board['id'] ?? 0); - if ($board_id <= 0) continue; - $moderators = $this->get_board_moderators($board_id); - if (in_array($user_id, $moderators, true)) { - $roles = $this->get_moderator_roles($board_id); - $boards[] = [ - 'board_id' => $board_id, - 'board_name' => (string) ($board['name'] ?? ''), - 'role' => $roles[$user_id] ?? 'moderator', - ]; - } - } - return $boards; - } - - /** - * Check if a user has a specific minimum tier on a board/plate. - * - * Tier hierarchy (aligned with zibll 4-level): - * forum_admin(4) > cat_moderator(3) > plate_author(2) > moderator(1) - * - * @param string $min_role 'moderator'|'plate_author'|'cat_moderator'|'forum_admin' - */ - public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool { - $tier_map = [ - 'junior_moderator' => Forum::TIER_MODERATOR, // backward-compat alias - 'moderator' => Forum::TIER_MODERATOR, - 'plate_author' => Forum::TIER_PLATE_AUTHOR, - 'super_moderator' => Forum::TIER_PLATE_AUTHOR, // backward-compat alias - 'cat_moderator' => Forum::TIER_CAT_MOD, - 'forum_admin' => Forum::TIER_FORUM_ADMIN, - ]; - $required = $tier_map[$min_role] ?? Forum::TIER_MODERATOR; - return $this->forum->get_user_plate_tier($user_id, $board_id) >= $required; - } - - /** - * Get board moderator applications (统一入口,自动检测 CPT/taxonomy). - */ - public function get_board_moderator_applications(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - return $this->get_plate_moderator_applications($board_id); - } - - // Fallback: taxonomy - $applications = get_term_meta($board_id, '_aksrt_moderator_applications', true); - if (!is_array($applications)) { - return []; - } - - return array_values(array_filter(array_map(function ($application) use ($board_id): array { - if (!is_array($application)) { - $application = []; - } - - return [ - 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), - 'board_id' => $board_id, - 'user_id' => (int) ($application['user_id'] ?? 0), - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), - ]; - }, $applications))); - } - - /** - * Get plate moderator applications from CPT meta. - */ - public function get_plate_moderator_applications(int $plate_id): array { - if (!post_type_exists('aksrt_forum_plate')) { - return []; - } - - $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); - if (!is_array($applications)) { - return []; - } - - return array_values(array_filter(array_map(function ($application) use ($plate_id): array { - if (!is_array($application)) { - $application = []; - } - - return [ - 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), - 'board_id' => $plate_id, - 'user_id' => (int) ($application['user_id'] ?? 0), - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), - ]; - }, $applications))); - } - - /** - * Get current user's board application. - */ - public function get_board_application_for_user(int $user_id, int $board_id): ?array { - $applications = $this->get_board_moderator_applications($board_id); - $matched = array_values(array_filter($applications, static function ($application) use ($user_id): bool { - return (int) ($application['user_id'] ?? 0) === $user_id; - })); - - if (empty($matched)) { - return null; - } - - usort($matched, static function ($left, $right): int { - return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); - }); - - $application = $matched[0]; - $application['status_label'] = $this->get_application_status_label((string) ($application['status'] ?? '')); - - return $application; - } - - public function find_application_by_id(string $application_id): ?array { - foreach ($this->get_moderator_application_list() as $application) { - if ((string) ($application['id'] ?? '') === $application_id) { - return $application; - } - } - - return null; - } - - public function get_application_status_label(string $status): string { - $map = [ - Forum::APPLICATION_PENDING => __('待处理', 'aksrt'), - Forum::APPLICATION_APPROVED => __('已通过', 'aksrt'), - Forum::APPLICATION_REJECTED => __('已拒绝', 'aksrt'), - ]; - - return $map[$status] ?? $map[Forum::APPLICATION_PENDING]; - } - - public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void { - if (!$this->forum->is_plate_cpt_active()) { - return; - } - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $this->sync_plate_counts($plate_id); - } - } - - public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void { - if (!$this->forum->is_plate_cpt_active()) { - return; - } - $post_id = (int) ($commentdata['comment_post_ID'] ?? 0); - if ($post_id <= 0) { - return; - } - $post = get_post($post_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $this->sync_plate_counts($plate_id); - } - } - - public function is_moderator(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_forum_admin($user_id)) { - return true; - } - - // CPT mode: resolve plate from topic meta - if ($this->forum->is_plate_cpt_active()) { - $plate_id = $this->forum->get_topic_plate_id($topic_id); - if ($plate_id > 0) { - return $this->forum->get_user_plate_tier($user_id, $plate_id) >= self::TIER_MODERATOR; - } - } - - // Fallback: taxonomy-based boards - $boards = get_the_terms($topic_id, 'aksrt_forum_board'); - if ($boards && !is_wp_error($boards)) { - foreach ($boards as $board) { - if ($this->is_board_moderator($user_id, (int) $board->term_id)) { - return true; - } - } - } - - return false; - } -} diff --git a/src/AKSRT/Forum/PermissionService.php b/src/AKSRT/Forum/PermissionService.php deleted file mode 100644 index 2b5d65db..00000000 --- a/src/AKSRT/Forum/PermissionService.php +++ /dev/null @@ -1,420 +0,0 @@ - cap_key => bool */ - private static array $cap_cache = []; - /** @var array "uid:plate_id" => tier */ - private static array $tier_cache = []; - - /** @var Forum */ - private $forum; - - public function __construct($forum) { - $this->forum = $forum; - } - - // ── Capability matrix ───────────────────────────────────────── - - /** - * Capability definitions matrix. - * - * Keys: - * 'min_tier' — minimum TIER_* constant required - * 'context' — 'topic'|'plate'|'global'|'any' - * 'self' — true = only applies to own content - * 'logged' — shorthand: any logged-in user - * 'settings_key' — optional forum settings permissions key - */ - private function cap_defs(): array { - return [ - // ── Topic viewing ───────────────────────────────────────────── - 'topic_view_pending' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_view_hidden' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_view_deleted' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - - // ── Topic editing ───────────────────────────────────────────── - 'topic_edit' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_delete' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - - // ── Topic governance ────────────────────────────────────────── - 'topic_sticky_plate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_sticky_super' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], - 'topic_sticky_global' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - 'topic_featured' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_lock' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_lock_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_highlight' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_score_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_move' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_moderate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - - // ── Topic interaction (logged users) ────────────────────────── - 'topic_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_favorite' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Reply governance ────────────────────────────────────────── - 'reply_adopt' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'reply_adopt_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_top' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - // ── Reply interaction ───────────────────────────────────────── - 'reply_like' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'reply_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Plate management ────────────────────────────────────────── - 'plate_follow' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'plate_create' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - 'plate_manage' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], - 'plate_delete' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - - // ── Moderator system ────────────────────────────────────────── - 'moderator_manage' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], - 'moderator_apply' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'moderator_review' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], - - // ── User governance ─────────────────────────────────────────── - 'user_ban' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'global'], - 'user_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Compose capabilities ────────────────────────────────────── - 'compose_post' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => ''], - 'compose_post_no_review' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'compose_upload_image' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], - 'compose_upload_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_video'], - 'compose_upload_file' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_file'], - 'compose_iframe_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_iframe_video'], - 'compose_set_type' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_visibility' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_cover' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], - 'compose_set_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_paid' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], - 'compose_set_hide' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], - ]; - } - - // ── Tier resolution ─────────────────────────────────────────── - - /** - * Return the user's highest tier for a given plate. - * - * Result is cached per request on "$user_id:$plate_id". - */ - public function get_user_plate_tier(int $user_id, int $plate_id): int { - if ($user_id <= 0) { - return self::TIER_NONE; - } - - $cache_key = "$user_id:$plate_id"; - if (isset(self::$tier_cache[$cache_key])) { - return self::$tier_cache[$cache_key]; - } - - $tier = $this->compute_user_plate_tier($user_id, $plate_id); - self::$tier_cache[$cache_key] = $tier; - return $tier; - } - - private function compute_user_plate_tier(int $user_id, int $plate_id): int { - // Tier 4: WP admin or bbs_admin_users list - if ($this->forum->is_forum_admin($user_id)) { - return self::TIER_FORUM_ADMIN; - } - - // Tier 3: category moderator (plate_cat term meta 'moderator') - $cats = get_the_terms($plate_id, 'aksrt_plate_cat'); - if ($cats && !is_wp_error($cats)) { - foreach ($cats as $cat) { - if ($this->forum->is_cat_moderator($user_id, (int) $cat->term_id)) { - return self::TIER_CAT_MOD; - } - } - } - - // Tier 2: plate_author (plate CPT post_author) - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($plate_id); - if ($post && (int) $post->post_author === $user_id) { - return self::TIER_PLATE_AUTHOR; - } - - // Tier 1: in plate _aksrt_moderators list - $mods = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - return self::TIER_MODERATOR; - } - - return self::TIER_NONE; - } - // Fallback: taxonomy board - $roles = get_term_meta($plate_id, '_aksrt_moderator_roles', true); - $roles = is_array($roles) ? $roles : []; - $role = $roles[$user_id] ?? ''; - if ($role === 'super_moderator' || $role === 'plate_author') { - return self::TIER_PLATE_AUTHOR; - } - $mods = get_term_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - return self::TIER_MODERATOR; - } - - return self::TIER_NONE; - } - - // ── Capability check ────────────────────────────────────────── - - /** - * Unified capability resolver. - * - * @param int $user_id User to check (0 = guest). - * @param string $cap Capability id from cap_defs(). - * @param int $context_id topic_id for 'topic' context, plate_id for 'plate' context. - * @return bool - */ - public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool { - $cache_key = "$user_id:$cap:$context_id"; - if (array_key_exists($cache_key, self::$cap_cache)) { - return self::$cap_cache[$cache_key]; - } - - $result = $this->compute_forum_cap($user_id, $cap, $context_id); - self::$cap_cache[$cache_key] = $result; - return $result; - } - - private function compute_forum_cap(int $user_id, string $cap, int $context_id): bool { - $defs = $this->cap_defs(); - if (!isset($defs[$cap])) { - return false; - } - $def = $defs[$cap]; - - $logged_only = !empty($def['logged']) && ($def['min_tier'] ?? self::TIER_NONE) === self::TIER_NONE; - $self_only = !empty($def['self']); - $context = $def['context'] ?? 'any'; - $min_tier = (int) ($def['min_tier'] ?? self::TIER_NONE); - $settings_key = $def['settings_key'] ?? ''; - // Guest short-circuit - if ($user_id <= 0) { - return false; - } - - // Forum-wide admin bypasses everything - if ($this->forum->is_forum_admin($user_id)) { - // Honour settings-gated compose caps even for admins when the feature is globally off - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (isset($perms[$settings_key]) && $perms[$settings_key] === false) { - return false; - } - } - return true; - } - - // 'logged' / any-user caps: just need to be logged in - if ($logged_only || ($min_tier === self::TIER_NONE && $context === 'any')) { - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { - return false; - } - } - return true; - } - - // 'self' caps: caller passes topic_id as context; check authorship - if ($self_only) { - if ($context_id <= 0) { - return false; - } - $post = get_post($context_id); - return $post && (int) $post->post_author === $user_id; - } - - // Global caps (no specific plate/topic context) - if ($context === 'global') { - if ($min_tier <= self::TIER_CAT_MOD) { - return $this->is_cat_moderator_anywhere($user_id); - } - return false; - } - - // Plate-context caps - if ($context === 'plate') { - $plate_id = $this->resolve_plate_id($context_id, $cap); - if ($plate_id <= 0) { - return false; - } - $tier = $this->get_user_plate_tier($user_id, $plate_id); - - // Check optional settings gate - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { - return $tier >= self::TIER_PLATE_AUTHOR; - } - } - - return $tier >= $min_tier; - } - - return false; - } - - // ── Helper methods ──────────────────────────────────────────── - - /** - * Resolve a plate_id from either a plate_id or topic_id, depending on the cap context. - */ - private function resolve_plate_id(int $context_id, string $cap): int { - if ($context_id <= 0) { - return 0; - } - $post = get_post($context_id); - if (!$post) { - return 0; - } - if ($post->post_type === 'aksrt_forum_plate') { - return $context_id; - } - if ($post->post_type === 'aksrt_forum_topic') { - return $this->forum->get_topic_plate_id($context_id); - } - return 0; - } - - /** - * Check if the user is a cat_moderator on ANY plate category (for global caps). - */ - private function is_cat_moderator_anywhere(int $user_id): bool { - if ($user_id <= 0) { - return false; - } - if ($this->forum->is_forum_admin($user_id)) { - return true; - } - $cats = get_terms(['taxonomy' => 'aksrt_plate_cat', 'hide_empty' => false, 'fields' => 'ids']); - if (is_wp_error($cats) || empty($cats)) { - return false; - } - foreach ((array) $cats as $cat_id) { - if ($this->forum->is_cat_moderator($user_id, (int) $cat_id)) { - return true; - } - } - return false; - } - - // ── Compose capabilities ────────────────────────────────────── - - /** - * Get per-feature capabilities for the compose/edit form. - * - * @return array - */ - public function get_compose_capabilities(int $user_id): array { - $keys = [ - 'can_post', 'can_set_type', 'can_set_visibility', - 'can_set_cover', 'can_set_cover_image', 'can_set_cover_slide', 'can_set_cover_video', - 'can_set_vote', 'can_set_paid', 'can_set_hide', - 'can_upload_image', 'can_upload_video', 'can_upload_file', - 'can_iframe_video', - ]; - if ($user_id <= 0) { - return array_fill_keys($keys, false); - } - - return [ - 'can_post' => $this->forum_user_can($user_id, 'compose_post'), - 'can_set_type' => $this->forum_user_can($user_id, 'compose_set_type'), - 'can_set_visibility' => $this->forum_user_can($user_id, 'compose_set_visibility'), - 'can_set_cover' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_image' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_slide' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_video' => $this->forum_user_can($user_id, 'compose_upload_video'), - 'can_set_vote' => $this->forum_user_can($user_id, 'compose_set_vote'), - 'can_set_paid' => $this->forum_user_can($user_id, 'compose_set_paid'), - 'can_set_hide' => $this->forum_user_can($user_id, 'compose_set_hide'), - 'can_upload_image' => $this->forum_user_can($user_id, 'compose_upload_image'), - 'can_upload_video' => $this->forum_user_can($user_id, 'compose_upload_video'), - 'can_upload_file' => $this->forum_user_can($user_id, 'compose_upload_file'), - 'can_iframe_video' => $this->forum_user_can($user_id, 'compose_iframe_video'), - ]; - } - - // ── Cache invalidation ──────────────────────────────────────── - - public function flush_cap_cache(int $user_id = 0): void { - if ($user_id > 0) { - foreach (array_keys(self::$cap_cache) as $k) { - if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { - unset(self::$cap_cache[$k]); - } - } - foreach (array_keys(self::$tier_cache) as $k) { - if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { - unset(self::$tier_cache[$k]); - } - } - } else { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - public function cap_cache_on_login(string $user_login, \WP_User $user): void { - $this->flush_cap_cache((int) $user->ID); - } - - /** Flush cache when plate moderator meta changes. */ - public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void { - // deleted_post_meta passes $meta_ids as array; other hooks pass int meta_id. - if (is_array($meta_key) || (is_array($meta_id) && !is_string($meta_key))) { - // Still accept call; $meta_key should be string on WP hooks. - } - $meta_key = is_string($meta_key) ? $meta_key : ''; - if (in_array($meta_key, ['_aksrt_moderators', '_aksrt_moderator_roles'], true)) { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - /** Flush cache when plate_cat moderator term meta changes. */ - public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $meta_key = is_string($meta_key) ? $meta_key : ''; - if ($meta_key === 'moderator') { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - /** Flush cache when a plate CPT post is saved (plate_author may change). */ - public function cap_cache_on_plate_save($post_id): void { - $post_id = (int) $post_id; - foreach (array_keys(self::$tier_cache) as $k) { - if (substr($k, -strlen(":$post_id")) === ":$post_id") { - unset(self::$tier_cache[$k]); - } - } - self::$cap_cache = []; - } -} diff --git a/src/AKSRT/Forum/PlateAdminList.php b/src/AKSRT/Forum/PlateAdminList.php deleted file mode 100644 index 478f688f..00000000 --- a/src/AKSRT/Forum/PlateAdminList.php +++ /dev/null @@ -1,107 +0,0 @@ - $label) { - $new[$key] = $label; - if ($key === 'title') { - $new['aksrt_plate_mods'] = __('版主数', 'aksrt'); - $new['aksrt_plate_posts'] = __('帖子数', 'aksrt'); - $new['aksrt_plate_reply'] = __('回复数', 'aksrt'); - $new['aksrt_plate_views'] = __('浏览', 'aksrt'); - $new['aksrt_plate_follow'] = __('关注', 'aksrt'); - $new['aksrt_plate_pending_apps'] = __('待审申请', 'aksrt'); - } - } - return $new; - } - - public function sortable_columns(array $columns): array { - $columns['aksrt_plate_posts'] = 'aksrt_posts_count'; - $columns['aksrt_plate_views'] = 'aksrt_plate_views'; - return $columns; - } - - public function render_column(string $column, int $post_id): void { - switch ($column) { - case 'aksrt_plate_mods': - $mods = get_post_meta($post_id, '_aksrt_moderators', true); - $count = is_array($mods) ? count(array_filter(array_map('intval', $mods))) : 0; - echo esc_html((string) $count); - break; - case 'aksrt_plate_posts': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_posts_count', true)); - break; - case 'aksrt_plate_reply': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_reply_count', true)); - break; - case 'aksrt_plate_views': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_plate_views', true)); - break; - case 'aksrt_plate_follow': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_follow_count', true)); - break; - case 'aksrt_plate_pending_apps': - $apps = get_post_meta($post_id, '_aksrt_moderator_applications', true); - $pending = 0; - if (is_array($apps)) { - foreach ($apps as $app) { - if (is_array($app) && (($app['status'] ?? '') === 'pending')) { - $pending++; - } - } - } - if ($pending > 0) { - echo '' . esc_html((string) $pending) . ''; - } else { - echo '0'; - } - break; - } - } - - public function apply_orderby(\WP_Query $query): void { - if (!is_admin() || !$query->is_main_query()) { - return; - } - if ($query->get('post_type') !== 'aksrt_forum_plate') { - return; - } - $orderby = $query->get('orderby'); - if ($orderby === 'aksrt_posts_count') { - $query->set('meta_key', '_aksrt_posts_count'); - $query->set('orderby', 'meta_value_num'); - } elseif ($orderby === 'aksrt_plate_views') { - $query->set('meta_key', '_aksrt_plate_views'); - $query->set('orderby', 'meta_value_num'); - } - } -} diff --git a/src/AKSRT/Forum/PlateFollowPay.php b/src/AKSRT/Forum/PlateFollowPay.php deleted file mode 100644 index ef50b956..00000000 --- a/src/AKSRT/Forum/PlateFollowPay.php +++ /dev/null @@ -1,852 +0,0 @@ -\d+)', [ - [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_preview'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - [ - 'methods' => 'POST', - 'callback' => [$this, 'rest_purchase'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - ]); - } - - /** - * @return true|\WP_Error - */ - public function check_rest_nonce(\WP_REST_Request $request) { - $nonce = $request->get_header('x-wp-nonce'); - if (!$nonce) { - return new \WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]); - } - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]); - } - return true; - } - - public function rest_preview(\WP_REST_Request $request) { - $plate_id = (int) $request['id']; - $uid = get_current_user_id(); - $coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? '')); - $preview = $this->preview($plate_id, $uid, $coupon_code); - if (is_wp_error($preview)) { - return $preview; - } - return new \WP_REST_Response($preview, 200); - } - - public function rest_purchase(\WP_REST_Request $request) { - $plate_id = (int) $request['id']; - $uid = get_current_user_id(); - $post = get_post($plate_id); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($uid <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), - ]); - } - - $params = $request->get_json_params(); - if (!is_array($params)) { - $params = $request->get_params(); - } - - $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); - if ($payment_method === '' || $payment_method === 'cash') { - $payment_method = 'balance'; - } - if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - $coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? '')); - - return $this->rest_wrap_result( - $this->create_cash_order($plate_id, $uid, $payment_method, $coupon_code) - ); - } - - /** - * @param mixed $result - * @return \WP_REST_Response|\WP_Error - */ - private function rest_wrap_result($result) { - if (is_wp_error($result)) { - return $result; - } - if (empty($result['success'])) { - $code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed')); - $status = (int) ($result['status'] ?? 400); - return new \WP_Error( - $code !== '' ? $code : 'aksrt_pay_failed', - (string) ($result['message'] ?? __('支付失败', 'aksrt')), - array_merge( - ['status' => $status > 0 ? $status : 400], - is_array($result['data'] ?? null) ? $result['data'] : [] - ) - ); - } - return new \WP_REST_Response($result, 200); - } - - /** - * @return array|\WP_Error - */ - public function preview(int $plate_id, int $user_id = 0, string $coupon_code = '') { - $assert = $this->assert_plate($plate_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - $uid = $user_id > 0 ? $user_id : get_current_user_id(); - $coupon_code = sanitize_text_field($coupon_code); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - - if ($uid <= 0 && !$allow_guest) { - return [ - 'post_id' => $plate_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'plate_follow', - 'pay_type_label' => __('付费关注', 'aksrt'), - 'amount' => 0, - 'base' => 0, - 'list_amount' => 0, - 'payable' => 0, - 'currency' => '¥', - 'amount_label' => '', - 'base_label' => '', - 'list_amount_label' => '', - 'is_free' => false, - 'has_paid_access' => false, - 'need_login' => true, - 'allow_guest' => false, - 'need_vip' => false, - 'need_purchase' => true, - 'logged_in' => false, - 'balance' => 0, - 'balance_enough' => false, - 'payment_methods' => [], - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => false, - 'coupon_desc' => '', - 'coupon_code' => '', - 'coupon' => null, - 'coupon_error' => '', - 'coupon_discount' => 0, - 'coupon_discount_label' => '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - ]; - } - - $priced = $this->resolve_price($plate_id, $uid); - if (($priced['modo'] ?? 'money') === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($uid <= 0) { - $vip_free = false; - } - $has_followed = $this->user_has_followed($uid, $plate_id); - - $wallet_balance = 0.0; - if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) { - $wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid); - } - - $methods = function_exists('aksrt_list_cash_payment_methods') - ? aksrt_list_cash_payment_methods($uid > 0) - : []; - - $coupon_allowed = $this->is_coupon_allowed($plate_id); - $coupon_error = ''; - $coupon_discount = 0.0; - $coupon_meta = null; - $applied_code = ''; - $payable = $list_amount; - - if (!$vip_free && $list_amount > 0) { - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, $coupon_code, true); - if (is_wp_error($discounted)) { - $coupon_error = $discounted->get_error_message(); - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, '', true); - } - if (!is_wp_error($discounted)) { - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $payable = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null; - $applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? '')); - if ($coupon_error === '' && !empty($discounted['coupon_error'])) { - $coupon_error = (string) $discounted['coupon_error']; - } - } - } elseif ($vip_free) { - $payable = 0.0; - $list_amount = 0.0; - } - - $is_free = $vip_free || $payable <= 0; - $amount_label = $is_free - ? __('免费', 'aksrt') - : sprintf('¥%s', number_format($payable, 2, '.', '')); - $list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', '')); - $base_label = sprintf('¥%s', number_format($base, 2, '.', '')); - - return [ - 'post_id' => $plate_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'plate_follow', - 'pay_type_label' => __('付费关注', 'aksrt'), - 'amount' => $payable, - 'base' => $base, - 'list_amount' => $list_amount > 0 ? $list_amount : $base, - 'list_amount_label' => $list_label, - 'payable' => $payable, - 'currency' => '¥', - 'amount_label' => $amount_label, - 'base_label' => $base_label, - 'is_free' => $is_free, - 'has_paid_access' => $has_followed, - 'paid_order' => null, - 'pending_order' => null, - 'need_login' => $uid <= 0 && !$allow_guest, - 'allow_guest' => $allow_guest, - 'need_vip' => false, - 'need_purchase' => !$has_followed && !$is_free, - 'logged_in' => $uid > 0, - 'balance' => $wallet_balance, - 'wallet_balance' => $wallet_balance, - 'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable), - 'payment_methods' => $methods, - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => $coupon_allowed, - 'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '', - 'coupon_code' => $applied_code, - 'coupon' => $coupon_meta, - 'coupon_error' => $coupon_error, - 'coupon_discount' => $coupon_discount, - 'coupon_discount_label' => $coupon_discount > 0 - ? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', ''))) - : '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'is_vip_price' => !empty($priced['is_vip_price']), - ]; - } - - /** - * @return array|\WP_Error - */ - public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') { - $assert = $this->assert_plate($plate_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($user_id <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url(get_permalink($post)), - ]); - } - - if ($this->user_has_followed($user_id, $plate_id)) { - return new \WP_Error('aksrt_already_followed', __('已关注该版块。', 'aksrt'), [ - 'status' => 400, - 'reload' => true, - ]); - } - - $priced = $this->resolve_price($plate_id, $user_id); - if (($priced['modo'] ?? 'money') === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($user_id <= 0) { - $vip_free = false; - } - $coupon_code = sanitize_text_field($coupon_code); - - // VIP free — no coupon stack. Guests never hit this. - if ($vip_free) { - return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [ - 'list_amount' => 0.0, - 'payable' => 0.0, - 'coupon_code' => '', - 'coupon_discount' => 0.0, - 'coupon' => [], - 'message' => __('VIP 免费关注成功', 'aksrt'), - 'paid_note' => __('VIP 免费关注版块。', 'aksrt'), - ]); - } - - if ($list_amount <= 0) { - return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]); - } - - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $user_id, $coupon_code, false); - if (is_wp_error($discounted)) { - return $discounted; - } - - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $amount = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : []; - $coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code); - - // Free after coupon. - if ($amount <= 0) { - return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [ - 'list_amount' => $list_amount, - 'payable' => 0.0, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'message' => __('无需支付,已关注。', 'aksrt'), - 'paid_note' => __('优惠后无需支付。', 'aksrt'), - ]); - } - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]); - } - - $payment_method = sanitize_key($payment_method); - if ($payment_method === '') { - $payment_method = $user_id > 0 ? 'balance' : 'alipay'; - } - if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - - $pending = $this->find_pending_order($user_id, $plate_id); - $can_resume = false; - if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) { - $pending_meta = $this->decode_order_note_meta($pending); - $pending_coupon = (string) ($pending_meta['coupon_code'] ?? ''); - if (is_array($pending_meta['coupon'] ?? null)) { - $pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon); - } - if ($pending_coupon === $coupon_code_norm) { - $can_resume = true; - } - } - - if ($can_resume) { - $order_number = (string) ($pending['order_number'] ?? ''); - $order = $pending; - } else { - $note = wp_json_encode([ - 'plate_follow_pay' => 1, - 'plate_id' => $plate_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => $base, - 'list_amount' => $list_amount, - 'amount' => $amount, - 'payable' => $amount, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'product_id' => $plate_id, - 'amount' => $amount, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - } - - $order['subject'] = sprintf( - /* translators: %s: plate title */ - __('付费关注版块「%s」', 'aksrt'), - get_the_title($post) - ); - $order['payment_method'] = $payment_method; - $order['amount'] = $amount; - - if (!class_exists('\AKSRT\Payment\Gateway')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]); - } - - $resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method); - if (empty($resolved['gateway'])) { - return new \WP_Error( - (string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'), - (string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')), - ['status' => 400, 'order_number' => $order_number] - ); - } - - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [ - 'paid_note' => __('用户完成版块付费关注支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return new \WP_Error( - (string) ($payment_result['code'] ?? 'aksrt_pay_failed'), - (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')), - ['status' => 402, 'order_number' => $order_number] - ); - } - - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => false, - 'poll' => true, - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => true, - ], - ]; - } - - public function on_order_paid(string $order_number, array $paid_order): void { - $product_type = sanitize_key((string) ($paid_order['product_type'] ?? '')); - if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW) { - return; - } - - $plate_id = (int) ($paid_order['product_id'] ?? 0); - $user_id = (int) ($paid_order['user_id'] ?? 0); - if ($plate_id <= 0) { - return; - } - - $meta = $this->decode_order_note_meta($paid_order); - if (empty($meta['plate_follow_pay'])) { - $post = get_post($plate_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') { - return; - } - } - - $fulfilled = $user_id <= 0; - if ($user_id > 0) { - $this->mark_followed($user_id, $plate_id); - $fulfilled = class_exists('AKSRT\\Forum\\Forum') - ? Forum::get_instance()->is_user_following_plate($user_id, $plate_id) - : in_array($plate_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_follow_plate', true)), true); - } elseif (function_exists('aksrt_set_guest_pay_cookies')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (is_array($full)) { - $paid_order = array_merge($full, $paid_order); - } - } - aksrt_set_guest_pay_cookies($paid_order); - } - $this->finalize_plate_order_side_effects($order_number, $paid_order); - if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('付费关注权益已发放。', 'aksrt')] - ); - } - } - - /** - * @return array{post:\WP_Post}|\WP_Error - */ - private function assert_plate(int $plate_id) { - $post = get_post($plate_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') { - return new \WP_Error('aksrt_invalid', __('版块不存在', 'aksrt'), ['status' => 404]); - } - if ($post->post_status !== 'publish') { - return new \WP_Error('aksrt_invalid', __('版块不可关注', 'aksrt'), ['status' => 404]); - } - - if (!$this->is_follow_pay_enabled($plate_id)) { - return new \WP_Error('aksrt_not_paid', __('该版块未开启付费关注', 'aksrt'), ['status' => 400]); - } - - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if ($modo === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - return ['post' => $post]; - } - - private function is_follow_pay_enabled(int $plate_id): bool { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_plate_follow_pay_enabled')) { - return (bool) Forum::get_instance()->is_plate_follow_pay_enabled($plate_id); - } - return (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; - } - - /** - * @return array{amount:float,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float,vip_level:int} - */ - private function resolve_price(int $plate_id, int $user_id): array { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'resolve_plate_follow_pay_price')) { - return Forum::get_instance()->resolve_plate_follow_pay_price($plate_id, $user_id); - } - return [ - 'amount' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), - 'modo' => 'money', - 'is_vip_price' => false, - 'is_free' => false, - 'label' => '', - 'base' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), - 'vip_level' => 0, - ]; - } - - private function user_has_followed(int $user_id, int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id); - } - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_user_following_plate')) { - return (bool) Forum::get_instance()->is_user_following_plate($user_id, $plate_id); - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - return false; - } - return in_array($plate_id, array_map('intval', $follow_list), true); - } - - private function mark_followed(int $user_id, int $plate_id): void { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_plate_followed')) { - Forum::get_instance()->mark_plate_followed($user_id, $plate_id); - return; - } - if ($user_id <= 0 || $plate_id <= 0) { - return; - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - } - - /** - * Free VIP or free-after-discount paid order. - * - * @param array{ - * list_amount?:float, - * payable?:float, - * coupon_code?:string, - * coupon_discount?:float, - * coupon?:array, - * message?:string, - * paid_note?:string - * } $opts - * @return array|\WP_Error - */ - private function create_free_paid_order(int $plate_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) { - $list_amount = round((float) ($opts['list_amount'] ?? 0), 2); - $payable = round((float) ($opts['payable'] ?? 0), 2); - $coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? '')); - $coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : []; - $message = (string) ($opts['message'] ?? __('VIP 免费关注成功', 'aksrt')); - $paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费关注版块。', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - $this->mark_followed($user_id, $plate_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $note = wp_json_encode([ - 'plate_follow_pay' => 1, - 'plate_id' => $plate_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => (float) ($priced['base'] ?? 0), - 'list_amount' => $list_amount, - 'amount' => $payable, - 'payable' => $payable, - 'coupon_code' => $coupon_code, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'free' => 1, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'product_id' => $plate_id, - 'amount' => 0.0, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => 'free', - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - $this->mark_followed($user_id, $plate_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'free', - 'payment_transaction_id' => 'free_' . $order_number, - 'note' => $paid_note, - ]); - if (!$paid) { - return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); - } - $this->mark_followed($user_id, $plate_id); - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - private function is_coupon_allowed(int $plate_id): bool { - return (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1; - } - - /** - * VIP list price → coupon → payable. - * - * @param array $priced - * @return array|\WP_Error - */ - private function apply_plate_cash_discounts(array $priced, int $plate_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) { - $list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2); - $pay = $list_amount; - $coupon_discount = 0.0; - $coupon_meta = []; - $coupon_error = ''; - $coupon_code = sanitize_text_field($coupon_code); - - if ($coupon_code !== '') { - if (!$this->is_coupon_allowed($plate_id)) { - $err = new \WP_Error('aksrt_coupon_not_allowed', __('当前版块不支持优惠码。', 'aksrt'), ['status' => 400]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } elseif (!class_exists('\AKSRT\Payment\Coupon')) { - $err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [ - 'user_id' => max(0, $user_id), - 'product_ids' => [$plate_id], - 'context' => 'plate_follow', - ]); - if (empty($result['valid'])) { - $err = new \WP_Error( - 'aksrt_coupon_error', - (string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')), - ['status' => 400] - ); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2); - if ($coupon_discount > $pay) { - $coupon_discount = $pay; - } - $pay = round(max(0.0, $pay - $coupon_discount), 2); - $c = is_array($result['coupon'] ?? null) ? $result['coupon'] : []; - $coupon_meta = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? $coupon_code), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'discount' => $coupon_discount, - ]; - $coupon_code = (string) $coupon_meta['code']; - } - } - } - - return [ - 'list_amount' => $list_amount, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'coupon_code' => $coupon_code, - 'coupon_error' => $coupon_error, - 'payable' => round(max(0.0, $pay), 2), - ]; - } - - private function finalize_plate_order_side_effects(string $order_number, array $order): void { - $meta = $this->decode_order_note_meta($order); - if (empty($meta['plate_follow_pay'])) { - return; - } - if (($meta['pay_modo'] ?? '') === 'points') { - return; - } - $buyer_id = (int) ($order['user_id'] ?? 0); - if (function_exists('aksrt_burn_order_coupon')) { - aksrt_burn_order_coupon($order_number, $meta, $buyer_id); - } - } - - private function find_pending_order(int $user_id, int $plate_id): ?array { - if ($user_id <= 0 || $plate_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) { - return null; - } - $orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [ - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'status' => \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING, - 'limit' => 20, - 'offset' => 0, - ]); - foreach ($orders as $order) { - if ((int) ($order['product_id'] ?? 0) !== $plate_id) { - continue; - } - $meta = $this->decode_order_note_meta($order); - if (!empty($meta['plate_follow_pay'])) { - return $order; - } - return $order; - } - return null; - } - - private function decode_order_note_meta(array $order): array { - if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) { - return $order['meta_payload']; - } - $note = (string) ($order['note'] ?? ''); - if ($note === '') { - return []; - } - $decoded = json_decode($note, true); - return is_array($decoded) ? $decoded : []; - } -} diff --git a/src/AKSRT/Forum/PlateMetaBox.php b/src/AKSRT/Forum/PlateMetaBox.php deleted file mode 100644 index b673ca57..00000000 --- a/src/AKSRT/Forum/PlateMetaBox.php +++ /dev/null @@ -1,581 +0,0 @@ - */ - private const ASSIGNABLE_ROLES = [ - 'super_moderator' => '超级版主', - 'moderator' => '版主', - 'junior_moderator' => '初级版主', - ]; - - /** @var list */ - private const ALLOW_VIEW = ['public', 'login', 'follow', 'role']; - - /** @var list */ - private const ADD_LIMIT = ['all', 'login', 'moderator', 'role']; - - public static function get_instance(): self { - if (null === self::$instance) { - self::$instance = new self(); - } - return self::$instance; - } - - public function init(): void { - if (!is_admin()) { - return; - } - add_action('add_meta_boxes', [$this, 'register_meta_box']); - add_action('save_post_aksrt_forum_plate', [$this, 'save'], 20, 2); - } - - public function register_meta_box(): void { - add_meta_box( - 'aksrt_plate_moderators', - __('版主与权限', 'aksrt'), - [$this, 'render'], - 'aksrt_forum_plate', - 'side', - 'high' - ); - } - - public function render(\WP_Post $post): void { - wp_nonce_field('aksrt_plate_meta_save', 'aksrt_plate_meta_nonce'); - - $plate_id = (int) $post->ID; - $author_id = (int) $post->post_author; - $moderator_ids = []; - $roles_map = []; - $applications = []; - - if (class_exists('AKSRT\\Forum\\Forum')) { - $forum = Forum::get_instance(); - $moderator_ids = $forum->get_plate_moderators($plate_id); - $roles_map = $forum->get_moderator_roles($plate_id); - $applications = $forum->get_plate_moderator_applications($plate_id); - } else { - $raw = get_post_meta($plate_id, '_aksrt_moderators', true); - $moderator_ids = is_array($raw) ? array_map('intval', $raw) : []; - $roles_raw = get_post_meta($plate_id, '_aksrt_moderator_roles', true); - $roles_map = is_array($roles_raw) ? $roles_raw : []; - $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); - $applications = is_array($applications) ? $applications : []; - } - - $allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'); - if (!in_array($allow_view, self::ALLOW_VIEW, true)) { - // Rewrite legacy values (e.g. moderator) on display; save will persist new contract. - $allow_view = 'login'; - } - $add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'); - if (!in_array($add_limit, self::ADD_LIMIT, true)) { - $add_limit = 'all'; - } - $view_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true); - $view_roles = is_array($view_roles) ? array_map('sanitize_key', $view_roles) : []; - $add_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); - $add_roles = is_array($add_roles) ? array_map('sanitize_key', $add_roles) : []; - $wp_roles = $this->wp_role_options(); - $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); - $sort_order = (int) get_post_meta($plate_id, '_aksrt_sort_order', true); - $follow_pay = $this->get_follow_pay_meta($plate_id); - - $this->render_access_fields($allow_view, $add_limit, $view_roles, $add_roles, $wp_roles); - $this->render_follow_pay_fields($follow_pay); - $this->render_display_fields($icon, $sort_order); - $this->render_moderator_fields($moderator_ids, $roles_map, $author_id); - $this->render_applications($applications); - } - - /** - * @param list $view_roles - * @param list $add_roles - * @param array $wp_roles - */ - private function render_access_fields(string $allow_view, string $add_limit, array $view_roles, array $add_roles, array $wp_roles): void { - ?> -

- - -

-

- - - -

-

- - -

-

- - - -

-
- -

- - -

-

- - - -

-

- - -

-

- - -

-

- - -

-

- - - -

-

- - -

-
- -

- - - -

-

- - - -

-
- $moderator_ids - * @param array $roles_map - */ - private function render_moderator_fields(array $moderator_ids, array $roles_map, int $author_id): void { - if ($author_id > 0) { - $author = get_userdata($author_id); - echo '

' . esc_html__('超级版主(版块作者)', 'aksrt') . '
'; - echo '' . esc_html($author ? $author->user_login . ' (#' . $author_id . ')' : (string) $author_id) . ''; - echo '
' . esc_html__('由版块作者决定,不可在此移除。', 'aksrt') . '

'; - } - - echo '

' . esc_html__('版主列表', 'aksrt') . '

'; - $rows = 0; - foreach ($moderator_ids as $uid) { - $uid = (int) $uid; - if ($uid <= 0 || $uid === $author_id) { - continue; - } - $user = get_userdata($uid); - $role = sanitize_key((string) ($roles_map[$uid] ?? 'moderator')); - if (!isset(self::ASSIGNABLE_ROLES[$role])) { - $role = 'moderator'; - } - $rows++; - echo '
'; - echo '
' . esc_html($user ? $user->user_login . ' (#' . $uid . ')' : (string) $uid) . '
'; - echo ''; - echo ''; - echo '
'; - } - if ($rows === 0) { - echo '

' . esc_html__('暂无版主。', 'aksrt') . '

'; - } - - echo '

' . esc_html__('添加版主', 'aksrt') . '

'; - echo '

'; - echo '

'; - echo '

' . esc_html__('保存后生效。勿将版块作者重复加为版主。', 'aksrt') . '

'; - echo '
'; - } - - /** - * @param array> $applications - */ - private function render_applications(array $applications): void { - echo '

' . esc_html__('版主申请', 'aksrt') . '

'; - $pending = array_values(array_filter($applications, static function ($app) { - if (!is_array($app)) { - return false; - } - $status = (string) ($app['status'] ?? ''); - if ($status === 'pending') { - return true; - } - return class_exists('AKSRT\\Forum\\Forum') && $status === Forum::APPLICATION_PENDING; - })); - if (!$pending) { - echo '

' . esc_html__('暂无待处理申请。', 'aksrt') . '

'; - return; - } - - echo '
    '; - foreach ($pending as $app) { - $uid = (int) ($app['user_id'] ?? 0); - $user = $uid > 0 ? get_userdata($uid) : null; - $aid = (string) ($app['id'] ?? ''); - $reason = (string) ($app['reason'] ?? ''); - echo '
  • '; - echo '
    ' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : (string) $uid) . '
    '; - if ($reason !== '') { - echo '
    ' . esc_html($reason) . '
    '; - } - echo ''; - echo ''; - echo ''; - echo '
  • '; - } - echo '
'; - } - - public function save(int $post_id, \WP_Post $post): void { - if (!isset($_POST['aksrt_plate_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_plate_meta_nonce'])), 'aksrt_plate_meta_save')) { - return; - } - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - if ($post->post_type !== 'aksrt_forum_plate' || !current_user_can('edit_post', $post_id)) { - return; - } - - $this->save_access_meta($post_id); - $this->save_follow_pay_meta($post_id); - $this->save_display_meta($post_id); - $this->save_moderators($post_id, (int) $post->post_author); - $this->save_applications(); - } - - private function save_display_meta(int $post_id): void { - $icon = isset($_POST['aksrt_plate_icon']) - ? sanitize_text_field((string) wp_unslash($_POST['aksrt_plate_icon'])) - : ''; - update_post_meta($post_id, '_aksrt_icon', $icon); - - $sort = isset($_POST['aksrt_plate_sort_order']) - ? (int) wp_unslash($_POST['aksrt_plate_sort_order']) - : 0; - update_post_meta($post_id, '_aksrt_sort_order', $sort); - } - - private function save_access_meta(int $post_id): void { - $allow_view = isset($_POST['aksrt_plate_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_allow_view'])) : 'public'; - if (!in_array($allow_view, self::ALLOW_VIEW, true)) { - $allow_view = 'public'; - } - update_post_meta($post_id, '_aksrt_allow_view', $allow_view); - - if ($allow_view === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_plate_allow_view_roles'] ?? []); - update_post_meta($post_id, '_aksrt_allow_view_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_allow_view_roles'); - } - - $add_limit = isset($_POST['aksrt_plate_add_limit']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_add_limit'])) : 'all'; - if (!in_array($add_limit, self::ADD_LIMIT, true)) { - $add_limit = 'all'; - } - update_post_meta($post_id, '_aksrt_add_limit', $add_limit); - - if ($add_limit === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_plate_add_limit_roles'] ?? []); - update_post_meta($post_id, '_aksrt_add_limit_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_add_limit_roles'); - } - } - - private function save_follow_pay_meta(int $post_id): void { - $enabled = !empty($_POST['aksrt_plate_follow_pay_enabled']); - $modo = isset($_POST['aksrt_plate_follow_pay_modo']) - ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_follow_pay_modo'])) - : 'money'; - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - $price = isset($_POST['aksrt_plate_follow_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_price'])) : 0; - $points = isset($_POST['aksrt_plate_follow_pay_points']) ? max(0, (int) wp_unslash($_POST['aksrt_plate_follow_pay_points'])) : 0; - - // Invalid price while enabled → force off (normalize, no migration). - if ($enabled) { - if ($modo === 'points' && $points <= 0) { - $enabled = false; - } - if ($modo === 'money' && $price <= 0) { - $enabled = false; - } - } - - if (!$enabled) { - delete_post_meta($post_id, '_aksrt_follow_pay'); - delete_post_meta($post_id, '_aksrt_follow_pay_modo'); - delete_post_meta($post_id, '_aksrt_follow_pay_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_points'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s'); - return; - } - - update_post_meta($post_id, '_aksrt_follow_pay', 1); - update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo); - update_post_meta($post_id, '_aksrt_follow_pay_price', $price); - update_post_meta($post_id, '_aksrt_follow_pay_points', $points); - update_post_meta($post_id, '_aksrt_follow_pay_coupon_s', !empty($_POST['aksrt_plate_follow_pay_coupon_s']) ? 1 : 0); - - if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') { - $vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price'])); - update_post_meta($post_id, '_aksrt_follow_pay_vip1_price', $vip1); - // Keep legacy single VIP key in sync with VIP1 for old readers. - update_post_meta($post_id, '_aksrt_follow_pay_vip_price', $vip1); - } else { - delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); - } - - if (isset($_POST['aksrt_plate_follow_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']) !== '') { - update_post_meta($post_id, '_aksrt_follow_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']))); - } else { - delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); - } - } - - /** - * @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string,coupon_s:bool} - */ - private function get_follow_pay_meta(int $plate_id): array { - $enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - - $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true); - $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); - if (!$vip1_set) { - // Legacy single VIP key → vip1. - $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true); - $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); - } - - $vip2_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true); - $vip2_set = ($vip2_raw !== '' && $vip2_raw !== null && $vip2_raw !== false); - - return [ - 'enabled' => $enabled, - 'modo' => $modo, - 'price' => (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true), - 'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true), - 'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '', - 'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '', - 'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1, - ]; - } - - private function save_moderators(int $post_id, int $author_id): void { - $role_posted = isset($_POST['aksrt_plate_mod_role']) && is_array($_POST['aksrt_plate_mod_role']) - ? wp_unslash($_POST['aksrt_plate_mod_role']) - : []; - $remove = isset($_POST['aksrt_plate_mod_remove']) && is_array($_POST['aksrt_plate_mod_remove']) - ? array_map('intval', wp_unslash($_POST['aksrt_plate_mod_remove'])) - : []; - $remove_set = array_fill_keys($remove, true); - - $desired = []; - foreach ($role_posted as $uid => $role) { - $uid = (int) $uid; - $role = sanitize_key((string) $role); - if ($uid <= 0 || $uid === $author_id || isset($remove_set[$uid])) { - continue; - } - if (!isset(self::ASSIGNABLE_ROLES[$role])) { - $role = 'moderator'; - } - $desired[$uid] = $role; - } - - $add_id = isset($_POST['aksrt_plate_mod_add_id']) ? (int) wp_unslash($_POST['aksrt_plate_mod_add_id']) : 0; - $add_role = isset($_POST['aksrt_plate_mod_add_role']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_mod_add_role'])) : 'moderator'; - if ($add_id > 0 && $add_id !== $author_id) { - if (!isset(self::ASSIGNABLE_ROLES[$add_role])) { - $add_role = 'moderator'; - } - $desired[$add_id] = $add_role; - } - - if (class_exists('AKSRT\\Forum\\Forum')) { - $forum = Forum::get_instance(); - $current = $forum->get_plate_moderators($post_id); - foreach ($current as $uid) { - $uid = (int) $uid; - if ($uid === $author_id) { - continue; - } - if (!isset($desired[$uid])) { - $forum->remove_moderator($uid, $post_id); - } - } - foreach ($desired as $uid => $role) { - if (in_array($uid, $current, true)) { - $forum->set_moderator_role($uid, $post_id, $role); - } else { - $forum->add_moderator($uid, $post_id, $role); - } - } - return; - } - - // Fallback without forum service: rewrite meta directly (no legacy keep). - $ids = array_values(array_unique(array_map('intval', array_keys($desired)))); - update_post_meta($post_id, '_aksrt_moderators', $ids); - update_post_meta($post_id, '_aksrt_moderator_roles', $desired); - } - - private function save_applications(): void { - $actions = isset($_POST['aksrt_plate_app']) && is_array($_POST['aksrt_plate_app']) - ? wp_unslash($_POST['aksrt_plate_app']) - : []; - if (!$actions || !class_exists('AKSRT\\Forum\\Forum')) { - return; - } - - $forum = Forum::get_instance(); - foreach ($actions as $application_id => $action) { - $application_id = sanitize_text_field((string) $application_id); - $action = sanitize_key((string) $action); - if ($application_id === '' || $action === 'keep') { - continue; - } - if ($action === 'approved') { - $forum->review_moderator_application($application_id, Forum::APPLICATION_APPROVED, '', get_current_user_id()); - } elseif ($action === 'rejected') { - $forum->review_moderator_application($application_id, Forum::APPLICATION_REJECTED, '', get_current_user_id()); - } - } - } - - /** - * @param mixed $raw - * @return list - */ - private function sanitize_role_list($raw): array { - if (!is_array($raw)) { - return []; - } - $valid = array_keys($this->wp_role_options()); - $out = []; - foreach ($raw as $role) { - $role = sanitize_key((string) $role); - if ($role !== '' && in_array($role, $valid, true)) { - $out[] = $role; - } - } - return array_values(array_unique($out)); - } - - /** - * @return array - */ - private function wp_role_options(): array { - $out = []; - if (!function_exists('wp_roles')) { - return $out; - } - foreach (wp_roles()->roles as $slug => $data) { - $out[sanitize_key((string) $slug)] = translate_user_role((string) ($data['name'] ?? $slug)); - } - return $out; - } -} diff --git a/src/AKSRT/Forum/ScoreService.php b/src/AKSRT/Forum/ScoreService.php deleted file mode 100644 index caeb75fc..00000000 --- a/src/AKSRT/Forum/ScoreService.php +++ /dev/null @@ -1,447 +0,0 @@ -forum = $forum; - } - - // ================================================================ - // Favorite (§7.3) - // ================================================================ - - public function is_favorited(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return false; - } - $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); - $favorites = is_array($favorites) ? $favorites : []; - return in_array($topic_id, $favorites, true); - } - - public function toggle_favorite(int $user_id, int $topic_id) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); - $favorites = is_array($favorites) ? $favorites : []; - $was_favorited = in_array($topic_id, $favorites, true); - - if ($was_favorited) { - $favorites = array_values(array_diff($favorites, [$topic_id])); - } else { - $favorites[] = $topic_id; - } - - update_user_meta($user_id, 'aksrt_favorite_forum_posts', $favorites); - - $count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); - $count = $was_favorited ? max(0, $count - 1) : $count + 1; - update_post_meta($topic_id, '_aksrt_favorite_count', $count); - $this->refresh_recommend_score($topic_id); - - return [ - 'favorited' => !$was_favorited, - 'count' => $count, - 'message' => $was_favorited - ? __('已取消收藏', 'aksrt') - : __('收藏成功', 'aksrt'), - ]; - } - - // ================================================================ - // Score (§7.3) - // ================================================================ - - public function score_topic(int $user_id, int $topic_id, int $amount) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - if ($amount === 0) { - return new \WP_Error('invalid_amount', __('评分值不能为零', 'aksrt')); - } - - $author_id = (int) $post->post_author; - if ($author_id === $user_id) { - return new \WP_Error('self_score', __('不能给自己的帖子评分', 'aksrt')); - } - - $config = $this->forum->get_config(); - $max_extra = (int) ($config['post_page']['score_extra_max'] ?? 5); - $max_deduct = (int) ($config['post_page']['score_deduct_max'] ?? 3); - - if ($amount > 0 && $amount > $max_extra) { - $amount = $max_extra; - } - if ($amount < 0 && abs($amount) > $max_deduct) { - $amount = -$max_deduct; - } - - $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); - $detail = is_array($detail) ? $detail : []; - - $user_key = (string) $user_id; - if (isset($detail[$user_key])) { - return new \WP_Error('already_scored', __('你已经对该帖评过分', 'aksrt')); - } - - $detail[$user_key] = [ - 'amount' => $amount, - 'time' => current_time('mysql'), - ]; - update_post_meta($topic_id, '_aksrt_score_detail', $detail); - - $total = (int) get_post_meta($topic_id, '_aksrt_score', true); - $total += $amount; - update_post_meta($topic_id, '_aksrt_score', $total); - do_action('aksrt_forum_topic_scored', $topic_id, (int) $author_id, (float) $amount); - - $reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true); - $reputation += $amount; - update_user_meta($author_id, 'aksrt_reputation', $reputation); - - if ($amount > 0) { - $credits_amount = (int) ($config['credits']['score_extra'] ?? 3); - if ($credits_amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_score_extra', $credits_amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - $daily = $this->today_credits_count($author_id, 'forum_score_extra'); - $daily_max = (int) ($config['credits']['daily_max'] ?? 50); - if ($daily < $daily_max) { - \AKSRT\Credits\Credits::add($author_id, $credits_amount, 'forum_score_extra', sprintf( - __('帖子「%s」被加分', 'aksrt'), - get_the_title($topic_id) - )); - } - } - } - } - - $this->refresh_recommend_score($topic_id); - $this->notify_topic_scored($topic_id, $user_id, $amount); - - return [ - 'score' => $total, - 'message' => $amount > 0 - ? __('加分成功', 'aksrt') - : __('减分成功', 'aksrt'), - ]; - } - - public function get_score_detail(int $topic_id): array { - $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); - $detail = is_array($detail) ? $detail : []; - - $entries = []; - foreach ($detail as $uid => $entry) { - $user = get_userdata((int) $uid); - $entries[] = [ - 'user_id' => (int) $uid, - 'display_name' => $user ? $user->display_name : '', - 'amount' => (int) ($entry['amount'] ?? 0), - 'time' => (string) ($entry['time'] ?? ''), - ]; - } - - return $entries; - } - - // ================================================================ - // Recommend score (§7.3) - // ================================================================ - - public function calculate_recommend_score(int $topic_id): float { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return 0.0; - } - - $views = (int) get_post_meta($topic_id, '_aksrt_views', true); - $favorites = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); - $comments = (int) get_comments([ - 'post_id' => $topic_id, - 'count' => true, - 'status' => 'approve', - ]); - $score = (int) get_post_meta($topic_id, '_aksrt_score', true); - - $w_views = 1.0; - $w_favorites = 5.0; - $w_comments = 3.0; - $w_score = 2.0; - - $raw = ($views * $w_views) - + ($favorites * $w_favorites) - + ($comments * $w_comments) - + (max(0, $score) * $w_score); - - $post_time = strtotime($post->post_date_gmt); - $age_hours = max(1, (time() - $post_time) / 3600); - $decay = pow($age_hours, 1.5); - $recommend = ($raw + 1) / $decay * 10000; - - return round($recommend, 2); - } - - public function refresh_recommend_score(int $topic_id): float { - $score = $this->calculate_recommend_score($topic_id); - update_post_meta($topic_id, '_aksrt_recommend_score', $score); - $this->maybe_reward_hot_topic($topic_id, $score); - return $score; - } - - private function maybe_reward_hot_topic(int $topic_id, float $score): void { - $config = $this->forum->get_config(); - $hot_threshold = (float) ($config['post_page']['hot_recommend_threshold'] - ?? $config['credits']['hot_recommend_threshold'] - ?? $config['other']['hot_topic_threshold'] - ?? 500); - if ($hot_threshold <= 0) { - $hot_threshold = 500.0; - } - if ($score < $hot_threshold) { - return; - } - if (get_post_meta($topic_id, '_aksrt_hot_rewarded', true)) { - return; - } - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $author_id = (int) $post->post_author; - if ($author_id <= 0) { - return; - } - do_action('aksrt_forum_topic_hot', $topic_id, (int) $author_id); - update_post_meta($topic_id, '_aksrt_hot_rewarded', '1'); - $amount = (int) ($config['credits']['posts_hot'] ?? 5); - if ($amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_posts_hot', $amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_topic', [ - 'type' => 'post', 'id' => $topic_id, - ]); - } - } - $this->notify($author_id, 'hot', sprintf( - __('你的帖子「%s」成为热帖!', 'aksrt'), get_the_title($topic_id) - ), [ - 'title' => __('热帖通知', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - public function batch_refresh_recommend_scores(int $limit = 100): int { - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'posts_per_page' => $limit, - 'orderby' => 'modified', - 'order' => 'DESC', - 'fields' => 'ids', - 'no_found_rows' => true, - ]); - - $count = 0; - foreach ($query->posts as $post_id) { - $this->refresh_recommend_score((int) $post_id); - $count++; - } - - return $count; - } - - public function maybe_refresh_recommend_on_view(): void { - if (!is_singular('aksrt_forum_topic')) { - return; - } - $topic_id = get_the_ID(); - if (!$topic_id) { - return; - } - $last = (int) get_post_meta($topic_id, '_aksrt_recommend_refreshed', true); - if ((time() - $last) > 300) { - $this->refresh_recommend_score($topic_id); - update_post_meta($topic_id, '_aksrt_recommend_refreshed', time()); - } - } - - private function get_bbs_type_label(string $bbs_type): string { - $config = $this->forum->get_config(); - $names = $config['names'] ?? []; - $map = [ - Forum::BBS_TYPE_STANDARD => $names['posts'] ?? __('帖子', 'aksrt'), - Forum::BBS_TYPE_QUESTION => $names['question_badge'] ?? __('提问', 'aksrt'), - ]; - return $map[$bbs_type] ?? $map[Forum::BBS_TYPE_STANDARD]; - } - - private function get_question_status_label(int $status): string { - $config = $this->forum->get_config(); - $names = $config['names'] ?? []; - return $status === Forum::QUESTION_SOLVED - ? ($names['question_ok_badge'] ?? __('已解决', 'aksrt')) - : ($names['question_badge'] ?? __('提问', 'aksrt')); - } - - // ================================================================ - // Notifications (§7.8) - // ================================================================ - - public function notify(int $user_id, string $type, string $content, array $data = []): void { - if ($user_id <= 0 || !class_exists('\AKSRT\Messages\Message')) { - return; - } - \AKSRT\Messages\Message::get_instance()->send($user_id, $type, $content, $data); - } - - public function notify_topic_pending(int $topic_id, int $author_id): void { - $title = get_the_title($topic_id); - $config = $this->forum->get_config(); - $admin_users = $config['admin_users'] ?? []; - - foreach ($admin_users as $admin_id) { - $this->notify((int) $admin_id, 'posts', sprintf( - __('帖子「%s」待审核', 'aksrt'), $title - ), [ - 'title' => __('待审核帖子', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - } - - public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $operator_id) return; - - $title = get_the_title($topic_id); - $action_labels = [ - 'review' => __('审核状态已变更', 'aksrt'), - 'sticky' => __('置顶状态已变更', 'aksrt'), - 'featured' => __('精华状态已变更', 'aksrt'), - 'locked' => __('锁定状态已变更', 'aksrt'), - 'close' => __('帖子已关闭', 'aksrt'), - 'hide' => __('帖子已隐藏', 'aksrt'), - 'delete' => __('帖子已删除', 'aksrt'), - 'undelete' => __('帖子已恢复', 'aksrt'), - ]; - - if ($action === 'review') { - $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); - if ($review_status === Forum::REVIEW_REJECTED) { - $action_labels['review'] = __('审核未通过', 'aksrt'); - } elseif ($review_status === Forum::REVIEW_PENDING) { - $action_labels['review'] = __('已转为待审核', 'aksrt'); - } elseif ($review_status === Forum::REVIEW_APPROVED) { - $action_labels['review'] = __('审核已通过', 'aksrt'); - } - } - - $label = $action_labels[$action] ?? __('治理动作已执行', 'aksrt'); - $target_url = $action === 'delete' ? aksrt_get_forum_home_url() : get_permalink($topic_id); - $payload = [ - 'title' => $label, - 'target_url' => $target_url, - ]; - - $note = (string) get_post_meta($topic_id, '_aksrt_moderation_note', true); - if ($note !== '') { - $payload['reason'] = $note; - } - - $this->notify($author_id, 'system', sprintf( - __('你的帖子「%1$s」%2$s', 'aksrt'), $title, $label - ), $payload); - } - - public function notify_topic_reply(int $topic_id, int $commenter_id): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $commenter_id) return; - - $commenter = get_userdata($commenter_id); - $name = $commenter ? $commenter->display_name : __('有人', 'aksrt'); - $title = get_the_title($topic_id); - - $this->notify($author_id, 'comment', sprintf( - __('%1$s 回复了你的帖子「%2$s」', 'aksrt'), $name, $title - ), [ - 'title' => __('新回复', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $scorer_id) return; - - $title = get_the_title($topic_id); - $this->notify($author_id, 'system', sprintf( - __('你的帖子「%1$s」被%2$s %3$d 分', 'aksrt'), - $title, - $amount > 0 ? __('加', 'aksrt') : __('减', 'aksrt'), - abs($amount) - ), [ - 'title' => $amount > 0 ? __('帖子被加分', 'aksrt') : __('帖子被减分', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - // ================================================================ - // Private helpers - // ================================================================ - - private function today_credits_count(int $user_id, string $reason): int { - if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) { - return 0; - } - - $table = \AKSRT\Core\DBMigrator::table('credit_log'); - if (!$table) { - return 0; - } - - global $wpdb; - $today = current_time('Y-m-d'); - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s", - $user_id, $reason, $today - )); - } -} diff --git a/src/AKSRT/Forum/TopicAdminList.php b/src/AKSRT/Forum/TopicAdminList.php deleted file mode 100644 index 9730cffa..00000000 --- a/src/AKSRT/Forum/TopicAdminList.php +++ /dev/null @@ -1,424 +0,0 @@ - $label) { - $new[$key] = $label; - if ($key === 'title') { - $new['aksrt_plate'] = __('版块', 'aksrt'); - $new['aksrt_sticky'] = __('置顶', 'aksrt'); - $new['aksrt_locked'] = __('锁定', 'aksrt'); - $new['aksrt_review'] = __('审核', 'aksrt'); - $new['aksrt_bbs_type']= __('类型', 'aksrt'); - $new['aksrt_visibility'] = __('可见性', 'aksrt'); - $new['aksrt_views'] = __('阅读', 'aksrt'); - } - } - return $new; - } - - public function sortable_columns(array $columns): array { - $columns['aksrt_sticky'] = 'aksrt_sticky'; - $columns['aksrt_views'] = 'aksrt_views'; - return $columns; - } - - public function render_column(string $column, int $post_id): void { - switch ($column) { - case 'aksrt_plate': - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $plate = get_post($plate_id); - echo $plate ? esc_html(get_the_title($plate)) : esc_html('#' . $plate_id); - } else { - // fallback board taxonomy - $terms = get_the_terms($post_id, 'aksrt_forum_board'); - if (is_array($terms) && $terms) { - echo esc_html($terms[0]->name); - } else { - echo '—'; - } - } - break; - - case 'aksrt_sticky': - $sticky = (int) get_post_meta($post_id, '_aksrt_sticky', true); - $map = [ - 0 => __('否', 'aksrt'), - 1 => __('版块', 'aksrt'), - 2 => __('超级', 'aksrt'), - 3 => __('全局', 'aksrt'), - ]; - echo esc_html($map[$sticky] ?? (string) $sticky); - echo ''; - break; - - case 'aksrt_locked': - $locked = (int) get_post_meta($post_id, '_aksrt_locked', true); - echo $locked ? esc_html__('是', 'aksrt') : esc_html__('否', 'aksrt'); - break; - - case 'aksrt_review': - $status = (string) get_post_meta($post_id, '_aksrt_review_status', true); - if ($status === '') { - $status = 'approved'; - } - $map = [ - 'approved' => __('已通过', 'aksrt'), - 'pending' => __('待审核', 'aksrt'), - 'rejected' => __('已拒绝', 'aksrt'), - ]; - echo esc_html($map[$status] ?? $status); - break; - - case 'aksrt_bbs_type': - $type = (string) (get_post_meta($post_id, '_aksrt_bbs_type', true) ?: 'standard'); - echo esc_html($type === 'question' ? __('提问', 'aksrt') : __('标准', 'aksrt')); - break; - - case 'aksrt_visibility': - $visibility = (string) (get_post_meta($post_id, '_aksrt_visibility', true) ?: 'public'); - $map = [ - 'public' => __('公开', 'aksrt'), - 'login' => __('登录', 'aksrt'), - 'comment' => __('评论', 'aksrt'), - 'role' => __('角色', 'aksrt'), - 'credits' => __('积分', 'aksrt'), - 'pay' => __('付费', 'aksrt'), - 'moderator' => __('版主', 'aksrt'), - ]; - echo esc_html($map[$visibility] ?? $visibility); - break; - - case 'aksrt_views': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_views', true)); - break; - } - } - - public function filters(string $post_type): void { - if ($post_type !== 'aksrt_forum_topic') { - return; - } - - $sticky = isset($_GET['aksrt_sticky']) ? sanitize_key((string) wp_unslash($_GET['aksrt_sticky'])) : ''; - $locked = isset($_GET['aksrt_locked']) ? sanitize_key((string) wp_unslash($_GET['aksrt_locked'])) : ''; - $review = isset($_GET['aksrt_review']) ? sanitize_key((string) wp_unslash($_GET['aksrt_review'])) : ''; - $plate = isset($_GET['aksrt_plate']) ? (int) $_GET['aksrt_plate'] : 0; - - echo ' '; - - echo ' '; - - echo ' '; - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => 200, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - if ($plates) { - echo ' '; - } - } - - public function apply_filters(\WP_Query $query): void { - if (!is_admin() || !$query->is_main_query()) { - return; - } - $post_type = $query->get('post_type'); - if ($post_type !== 'aksrt_forum_topic') { - return; - } - - $meta_query = (array) $query->get('meta_query'); - - if (isset($_GET['aksrt_sticky']) && $_GET['aksrt_sticky'] !== '') { - $meta_query[] = [ - 'key' => '_aksrt_sticky', - 'value' => (int) $_GET['aksrt_sticky'], - ]; - } - if (isset($_GET['aksrt_locked']) && $_GET['aksrt_locked'] !== '') { - $meta_query[] = [ - 'key' => '_aksrt_locked', - 'value' => (int) $_GET['aksrt_locked'], - ]; - } - if (!empty($_GET['aksrt_review'])) { - $review = sanitize_key((string) wp_unslash($_GET['aksrt_review'])); - if ($review === 'approved') { - $meta_query[] = [ - 'relation' => 'OR', - [ - 'key' => '_aksrt_review_status', - 'value' => 'approved', - ], - [ - 'key' => '_aksrt_review_status', - 'compare' => 'NOT EXISTS', - ], - [ - 'key' => '_aksrt_review_status', - 'value' => '', - ], - ]; - } else { - $meta_query[] = [ - 'key' => '_aksrt_review_status', - 'value' => $review, - ]; - } - } - if (!empty($_GET['aksrt_plate'])) { - $meta_query[] = [ - 'key' => '_aksrt_plate_id', - 'value' => (int) $_GET['aksrt_plate'], - ]; - } - - if ($meta_query) { - $query->set('meta_query', $meta_query); - } - - $orderby = $query->get('orderby'); - if ($orderby === 'aksrt_sticky') { - $query->set('meta_key', '_aksrt_sticky'); - $query->set('orderby', 'meta_value_num'); - } elseif ($orderby === 'aksrt_views') { - $query->set('meta_key', '_aksrt_views'); - $query->set('orderby', 'meta_value_num'); - } - } - - public function bulk_actions(array $actions): array { - $actions['aksrt_sticky_1'] = __('设为版块置顶', 'aksrt'); - $actions['aksrt_sticky_0'] = __('取消置顶', 'aksrt'); - $actions['aksrt_lock'] = __('锁定', 'aksrt'); - $actions['aksrt_unlock'] = __('解锁', 'aksrt'); - $actions['aksrt_approve'] = __('审核通过', 'aksrt'); - $actions['aksrt_reject'] = __('审核拒绝', 'aksrt'); - return $actions; - } - - public function handle_bulk_actions(string $redirect_to, string $action, array $post_ids): string { - $count = 0; - foreach ($post_ids as $post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0 || get_post_type($post_id) !== 'aksrt_forum_topic') { - continue; - } - switch ($action) { - case 'aksrt_sticky_1': - update_post_meta($post_id, '_aksrt_sticky', 1); - $count++; - break; - case 'aksrt_sticky_0': - update_post_meta($post_id, '_aksrt_sticky', 0); - $count++; - break; - case 'aksrt_lock': - update_post_meta($post_id, '_aksrt_locked', 1); - $count++; - break; - case 'aksrt_unlock': - update_post_meta($post_id, '_aksrt_locked', 0); - $count++; - break; - case 'aksrt_approve': - update_post_meta($post_id, '_aksrt_review_status', 'approved'); - $count++; - break; - case 'aksrt_reject': - update_post_meta($post_id, '_aksrt_review_status', 'rejected'); - $count++; - break; - } - } - - return add_query_arg('aksrt_topic_bulk', $count, $redirect_to); - } - - public function bulk_notices(): void { - if (empty($_GET['aksrt_topic_bulk'])) { - return; - } - $count = (int) $_GET['aksrt_topic_bulk']; - echo '

' . esc_html(sprintf( - /* translators: %d count */ - __('已更新 %d 个帖子。', 'aksrt'), - $count - )) . '

'; - } - - public function quick_edit_box(string $column_name, string $post_type): void { - if ($post_type !== 'aksrt_forum_topic' || $column_name !== 'aksrt_sticky') { - return; - } - ?> -
-
- - - - - -
-
- post_type !== 'aksrt_forum_topic') { - return; - } - ?> - - ID, '_aksrt_plate_id', true); - $sticky = (int) get_post_meta($post->ID, '_aksrt_sticky', true); - $locked = (int) get_post_meta($post->ID, '_aksrt_locked', true); - $featured = (int) get_post_meta($post->ID, '_aksrt_featured', true); - $review = (string) get_post_meta($post->ID, '_aksrt_review_status', true); - if ($review === '') { - $review = 'approved'; - } - $bbs_type = (string) (get_post_meta($post->ID, '_aksrt_bbs_type', true) ?: 'standard'); - $visibility = sanitize_key((string) (get_post_meta($post->ID, '_aksrt_visibility', true) ?: 'public')); - if (!in_array($visibility, self::VISIBILITY, true)) { - $visibility = 'public'; - } - - $credits_cost = (int) get_post_meta($post->ID, '_aksrt_credits_cost', true); - if ($credits_cost <= 0) { - // Legacy dual key. - $credits_cost = max(0, (int) get_post_meta($post->ID, '_aksrt_pay_credits', true)); - } - $pay_price = (float) get_post_meta($post->ID, '_aksrt_pay_price', true); - $pay_vip_price = get_post_meta($post->ID, '_aksrt_pay_vip_price', true); - $vip_set = ($pay_vip_price !== '' && $pay_vip_price !== null && $pay_vip_price !== false); - $vip_price_val = $vip_set ? (float) $pay_vip_price : ''; - $pay_vip1_price = get_post_meta($post->ID, '_aksrt_pay_vip1_price', true); - $pay_vip2_price = get_post_meta($post->ID, '_aksrt_pay_vip2_price', true); - $vip1_set = ($pay_vip1_price !== '' && $pay_vip1_price !== null && $pay_vip1_price !== false); - $vip2_set = ($pay_vip2_price !== '' && $pay_vip2_price !== null && $pay_vip2_price !== false); - // Migrate single VIP price into vip1 display if vip1 empty. - if (!$vip1_set && $vip_set) { - $vip1_set = true; - $pay_vip1_price = $pay_vip_price; - } - $credits_vip1 = get_post_meta($post->ID, '_aksrt_credits_vip1', true); - $credits_vip2 = get_post_meta($post->ID, '_aksrt_credits_vip2', true); - $credits_vip1_set = ($credits_vip1 !== '' && $credits_vip1 !== null && $credits_vip1 !== false); - $credits_vip2_set = ($credits_vip2 !== '' && $credits_vip2 !== null && $credits_vip2 !== false); - $virtual_sales = max(0, (int) get_post_meta($post->ID, '_aksrt_virtual_sales', true)); - $coupon_s = (int) get_post_meta($post->ID, '_aksrt_topic_coupon_s', true) === 1; - $pay_hide_part = (int) get_post_meta($post->ID, '_aksrt_pay_hide_part', true) === 1; - $view_roles = get_post_meta($post->ID, '_aksrt_visibility_roles', true); - if (!is_array($view_roles)) { - $view_roles = []; - } - $view_roles = array_values(array_filter(array_map('sanitize_key', $view_roles))); - $wp_roles = $this->wp_role_options(); - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => 200, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - - $visibility_labels = [ - 'public' => __('公开', 'aksrt'), - 'login' => __('登录可见', 'aksrt'), - 'comment' => __('评论可见', 'aksrt'), - 'role' => __('指定角色', 'aksrt'), - 'credits' => __('支付积分后可见', 'aksrt'), - 'pay' => __('付费后可见', 'aksrt'), - 'moderator' => __('仅版主', 'aksrt'), - ]; - ?> -

- - -

-

- - -

-

-
- -

-

- - -

-

- - -

-

- - -

-

- - - -

-

- - - -

-

- - -

-

- - -

-

- - -

-

- - -

-

- - - -

-

- - -

-

- - - -

-

- - -

- post_type !== 'aksrt_forum_topic' || !current_user_can('edit_post', $post_id)) { - return; - } - - $plate_id = isset($_POST['aksrt_plate_id']) ? max(0, (int) $_POST['aksrt_plate_id']) : 0; - update_post_meta($post_id, '_aksrt_plate_id', $plate_id); - - $sticky = isset($_POST['aksrt_sticky']) ? max(0, min(3, (int) $_POST['aksrt_sticky'])) : 0; - update_post_meta($post_id, '_aksrt_sticky', $sticky); - update_post_meta($post_id, '_aksrt_locked', !empty($_POST['aksrt_locked']) ? 1 : 0); - update_post_meta($post_id, '_aksrt_featured', !empty($_POST['aksrt_featured']) ? 1 : 0); - - $review = isset($_POST['aksrt_review_status']) ? sanitize_key((string) wp_unslash($_POST['aksrt_review_status'])) : 'approved'; - if (!in_array($review, ['approved', 'pending', 'rejected'], true)) { - $review = 'approved'; - } - update_post_meta($post_id, '_aksrt_review_status', $review); - - $bbs_type = isset($_POST['aksrt_bbs_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_bbs_type'])) : 'standard'; - if (!in_array($bbs_type, ['standard', 'question'], true)) { - $bbs_type = 'standard'; - } - update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type); - - $this->save_visibility_meta($post_id); - - // Keep board taxonomy bridge roughly in sync when plate selected. - if ($plate_id > 0 && taxonomy_exists('aksrt_forum_board')) { - $slug = 'plate-' . $plate_id; - $term = get_term_by('slug', $slug, 'aksrt_forum_board'); - if (!$term) { - $title = get_the_title($plate_id) ?: $slug; - $created = wp_insert_term($title, 'aksrt_forum_board', ['slug' => $slug]); - if (!is_wp_error($created) && !empty($created['term_id'])) { - $term_id = (int) $created['term_id']; - } else { - $term_id = 0; - } - } else { - $term_id = (int) $term->term_id; - } - if ($term_id > 0) { - wp_set_object_terms($post_id, [$term_id], 'aksrt_forum_board', false); - } - } - } - - private function save_visibility_meta(int $post_id): void { - $visibility = isset($_POST['aksrt_visibility']) ? sanitize_key((string) wp_unslash($_POST['aksrt_visibility'])) : 'public'; - if (!in_array($visibility, self::VISIBILITY, true)) { - $visibility = 'public'; - } - update_post_meta($post_id, '_aksrt_visibility', $visibility); - - // Rewrite legacy dual allow_view so admin/runtime stay on one contract. - $allow_view_map = [ - 'public' => 'public', - 'login' => 'login', - 'comment' => 'login', - 'role' => 'public', - 'credits' => 'pay', - 'pay' => 'pay', - 'moderator' => 'moderator', - ]; - update_post_meta($post_id, '_aksrt_allow_view', $allow_view_map[$visibility] ?? 'public'); - - // Roles. - if ($visibility === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_visibility_roles'] ?? []); - update_post_meta($post_id, '_aksrt_visibility_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_visibility_roles'); - } - - // Credits (canonical + legacy dual-write). - if ($visibility === 'credits') { - $cost = isset($_POST['aksrt_credits_cost']) ? max(0, (int) wp_unslash($_POST['aksrt_credits_cost'])) : 0; - // Accept legacy field name if UI still posts it. - if ($cost <= 0 && isset($_POST['aksrt_pay_credits'])) { - $cost = max(0, (int) wp_unslash($_POST['aksrt_pay_credits'])); - } - update_post_meta($post_id, '_aksrt_credits_cost', $cost); - update_post_meta($post_id, '_aksrt_pay_credits', $cost); - - if (isset($_POST['aksrt_credits_vip1']) && (string) wp_unslash($_POST['aksrt_credits_vip1']) !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) wp_unslash($_POST['aksrt_credits_vip1']))); - } else { - delete_post_meta($post_id, '_aksrt_credits_vip1'); - } - if (isset($_POST['aksrt_credits_vip2']) && (string) wp_unslash($_POST['aksrt_credits_vip2']) !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) wp_unslash($_POST['aksrt_credits_vip2']))); - } else { - delete_post_meta($post_id, '_aksrt_credits_vip2'); - } - } else { - delete_post_meta($post_id, '_aksrt_credits_cost'); - delete_post_meta($post_id, '_aksrt_pay_credits'); - delete_post_meta($post_id, '_aksrt_credits_vip1'); - delete_post_meta($post_id, '_aksrt_credits_vip2'); - } - - // Cash pay. - if ($visibility === 'pay') { - $price = isset($_POST['aksrt_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_pay_price'])) : 0; - update_post_meta($post_id, '_aksrt_pay_price', $price); - - $vip1_written = false; - if (isset($_POST['aksrt_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_pay_vip1_price']) !== '') { - $vip1 = max(0, (float) wp_unslash($_POST['aksrt_pay_vip1_price'])); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip1); - // Keep legacy single VIP key in sync with VIP1 for older readers. - update_post_meta($post_id, '_aksrt_pay_vip_price', $vip1); - $vip1_written = true; - } else { - delete_post_meta($post_id, '_aksrt_pay_vip1_price'); - } - if (isset($_POST['aksrt_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_pay_vip2_price']) !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_pay_vip2_price']))); - } else { - delete_post_meta($post_id, '_aksrt_pay_vip2_price'); - } - // Legacy field if posted without vip1. - if (!$vip1_written && isset($_POST['aksrt_pay_vip_price']) && (string) wp_unslash($_POST['aksrt_pay_vip_price']) !== '') { - $vip = max(0, (float) wp_unslash($_POST['aksrt_pay_vip_price'])); - update_post_meta($post_id, '_aksrt_pay_vip_price', $vip); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip); - } elseif (!$vip1_written) { - delete_post_meta($post_id, '_aksrt_pay_vip_price'); - } - } else { - delete_post_meta($post_id, '_aksrt_pay_price'); - delete_post_meta($post_id, '_aksrt_pay_vip_price'); - delete_post_meta($post_id, '_aksrt_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_pay_vip2_price'); - } - - // Partial hide only meaningful for pay/credits; otherwise clear. - if (in_array($visibility, ['pay', 'credits'], true)) { - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($_POST['aksrt_pay_hide_part']) ? 1 : 0); - $virtual = isset($_POST['aksrt_virtual_sales']) ? max(0, (int) wp_unslash($_POST['aksrt_virtual_sales'])) : 0; - if ($virtual > 0) { - update_post_meta($post_id, '_aksrt_virtual_sales', $virtual); - } else { - delete_post_meta($post_id, '_aksrt_virtual_sales'); - } - if ($visibility === 'pay') { - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($_POST['aksrt_topic_coupon_s']) ? 1 : 0); - } else { - delete_post_meta($post_id, '_aksrt_topic_coupon_s'); - } - } else { - delete_post_meta($post_id, '_aksrt_pay_hide_part'); - delete_post_meta($post_id, '_aksrt_virtual_sales'); - delete_post_meta($post_id, '_aksrt_topic_coupon_s'); - } - } - - /** - * @param mixed $raw - * @return string[] - */ - private function sanitize_role_list($raw): array { - if (!is_array($raw)) { - return []; - } - $allowed = array_keys($this->wp_role_options()); - $out = []; - foreach ($raw as $role) { - $role = sanitize_key((string) $role); - if ($role !== '' && in_array($role, $allowed, true)) { - $out[] = $role; - } - } - return array_values(array_unique($out)); - } - - /** - * @return array - */ - private function wp_role_options(): array { - if (!function_exists('wp_roles')) { - return []; - } - $roles = wp_roles(); - if (!$roles || !is_array($roles->roles)) { - return []; - } - $out = []; - foreach ($roles->roles as $key => $data) { - $out[sanitize_key((string) $key)] = (string) ($data['name'] ?? $key); - } - return $out; - } -} diff --git a/src/AKSRT/Forum/TopicPay.php b/src/AKSRT/Forum/TopicPay.php deleted file mode 100644 index ed89f0ae..00000000 --- a/src/AKSRT/Forum/TopicPay.php +++ /dev/null @@ -1,853 +0,0 @@ -\d+)', [ - [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_preview'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - [ - 'methods' => 'POST', - 'callback' => [$this, 'rest_purchase'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - ]); - } - - /** - * @param \WP_REST_Request $request - * @return true|\WP_Error - */ - public function check_rest_nonce(\WP_REST_Request $request) { - $nonce = $request->get_header('x-wp-nonce'); - if (!$nonce) { - return new \WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]); - } - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]); - } - return true; - } - - public function rest_preview(\WP_REST_Request $request) { - $topic_id = (int) $request['id']; - $uid = get_current_user_id(); - $coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? '')); - $preview = $this->preview($topic_id, $uid, $coupon_code); - if (is_wp_error($preview)) { - return $preview; - } - return new \WP_REST_Response($preview, 200); - } - - public function rest_purchase(\WP_REST_Request $request) { - $topic_id = (int) $request['id']; - $uid = get_current_user_id(); - $post = get_post($topic_id); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($uid <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), - ]); - } - - $params = $request->get_json_params(); - if (!is_array($params)) { - $params = $request->get_params(); - } - - $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); - if ($payment_method === '' || $payment_method === 'cash') { - $payment_method = 'balance'; - } - // Guests cannot use balance. - if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - $coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? '')); - - return $this->rest_wrap_result( - $this->create_cash_order($topic_id, $uid, $payment_method, $coupon_code) - ); - } - - /** - * @param mixed $result - * @return \WP_REST_Response|\WP_Error - */ - private function rest_wrap_result($result) { - if (is_wp_error($result)) { - return $result; - } - if (empty($result['success'])) { - $code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed')); - $status = (int) ($result['status'] ?? 400); - return new \WP_Error( - $code !== '' ? $code : 'aksrt_pay_failed', - (string) ($result['message'] ?? __('支付失败', 'aksrt')), - array_merge( - ['status' => $status > 0 ? $status : 400], - is_array($result['data'] ?? null) ? $result['data'] : [] - ) - ); - } - return new \WP_REST_Response($result, 200); - } - - /** - * Cashier preview payload (content-pay field subset for cms-download). - * - * @return array|\WP_Error - */ - public function preview(int $topic_id, int $user_id = 0, string $coupon_code = '') { - $assert = $this->assert_topic($topic_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - $uid = $user_id > 0 ? $user_id : get_current_user_id(); - $coupon_code = sanitize_text_field($coupon_code); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - - if ($uid <= 0 && !$allow_guest) { - return [ - 'post_id' => $topic_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'topic', - 'pay_type_label' => __('付费主题', 'aksrt'), - 'amount' => 0, - 'base' => 0, - 'list_amount' => 0, - 'payable' => 0, - 'currency' => '¥', - 'amount_label' => '', - 'base_label' => '', - 'list_amount_label' => '', - 'is_free' => false, - 'has_paid_access' => false, - 'need_login' => true, - 'allow_guest' => false, - 'need_vip' => false, - 'need_purchase' => true, - 'logged_in' => false, - 'balance' => 0, - 'balance_enough' => false, - 'payment_methods' => [], - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => false, - 'coupon_desc' => '', - 'coupon_code' => '', - 'coupon' => null, - 'coupon_error' => '', - 'coupon_discount' => 0, - 'coupon_discount_label' => '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - ]; - } - - $priced = $this->resolve_price($topic_id, $uid); - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - // Guests never get VIP free (vip_level=0). - if ($uid <= 0) { - $vip_free = false; - } - $has_paid = $this->user_has_paid($uid, $topic_id); - - $wallet_balance = 0.0; - if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) { - $wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid); - } - - $methods = function_exists('aksrt_list_cash_payment_methods') - ? aksrt_list_cash_payment_methods($uid > 0) - : []; - - $coupon_allowed = $this->is_coupon_allowed($topic_id); - $coupon_error = ''; - $coupon_discount = 0.0; - $coupon_meta = null; - $applied_code = ''; - $payable = $list_amount; - - if (!$vip_free && $list_amount > 0) { - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, $coupon_code, true); - if (is_wp_error($discounted)) { - $coupon_error = $discounted->get_error_message(); - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, '', true); - } - if (!is_wp_error($discounted)) { - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $payable = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null; - $applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? '')); - if ($coupon_error === '' && !empty($discounted['coupon_error'])) { - $coupon_error = (string) $discounted['coupon_error']; - } - } - } elseif ($vip_free) { - $payable = 0.0; - $list_amount = 0.0; - } - - $is_free = $vip_free || $payable <= 0; - $amount_label = $is_free - ? __('免费', 'aksrt') - : sprintf('¥%s', number_format($payable, 2, '.', '')); - $list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', '')); - $base_label = sprintf('¥%s', number_format($base, 2, '.', '')); - - return [ - 'post_id' => $topic_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'topic', - 'pay_type_label' => __('付费主题', 'aksrt'), - 'amount' => $payable, - 'base' => $base, - 'list_amount' => $list_amount > 0 ? $list_amount : $base, - 'list_amount_label' => $list_label, - 'payable' => $payable, - 'currency' => '¥', - 'amount_label' => $amount_label, - 'base_label' => $base_label, - 'is_free' => $is_free, - 'has_paid_access' => $has_paid, - 'paid_order' => null, - 'pending_order' => null, - 'need_login' => $uid <= 0 && !$allow_guest, - 'allow_guest' => $allow_guest, - 'need_vip' => false, - 'need_purchase' => !$has_paid && !$is_free, - 'logged_in' => $uid > 0, - 'balance' => $wallet_balance, - 'wallet_balance' => $wallet_balance, - 'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable), - 'payment_methods' => $methods, - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => $coupon_allowed, - 'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '', - 'coupon_code' => $applied_code, - 'coupon' => $coupon_meta, - 'coupon_error' => $coupon_error, - 'coupon_discount' => $coupon_discount, - 'coupon_discount_label' => $coupon_discount > 0 - ? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', ''))) - : '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'is_vip_price' => !empty($priced['is_vip_price']), - ]; - } - - /** - * Create cash order and invoke gateway. - * - * @return array|\WP_Error - */ - public function create_cash_order(int $topic_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') { - $assert = $this->assert_topic($topic_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($user_id <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url(get_permalink($post)), - ]); - } - - if ($this->user_has_paid($user_id, $topic_id)) { - return new \WP_Error('aksrt_already_paid', __('已购买,可直接查看。', 'aksrt'), [ - 'status' => 400, - 'has_paid_access' => true, - 'reload' => true, - ]); - } - - $priced = $this->resolve_price($topic_id, $user_id); - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($user_id <= 0) { - $vip_free = false; - } - $coupon_code = sanitize_text_field($coupon_code); - - // VIP free (no purchase price) — no coupon stack. Guests never hit this. - if ($vip_free) { - return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [ - 'list_amount' => 0.0, - 'payable' => 0.0, - 'coupon_code' => '', - 'coupon_discount' => 0.0, - 'coupon' => [], - 'message' => __('VIP 免费解锁成功', 'aksrt'), - 'paid_note' => __('VIP 免费解锁主题。', 'aksrt'), - ]); - } - - if ($list_amount <= 0) { - return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]); - } - - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $user_id, $coupon_code, false); - if (is_wp_error($discounted)) { - return $discounted; - } - - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $amount = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : []; - $coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code); - - // Free after coupon discount. - if ($amount <= 0) { - return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [ - 'list_amount' => $list_amount, - 'payable' => 0.0, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'message' => __('无需支付,已可访问。', 'aksrt'), - 'paid_note' => __('优惠后无需支付。', 'aksrt'), - ]); - } - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]); - } - - $payment_method = sanitize_key($payment_method); - if ($payment_method === '') { - $payment_method = $user_id > 0 ? 'balance' : 'alipay'; - } - if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - - $pending = $this->find_pending_topic_order($user_id, $topic_id); - $can_resume = false; - if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) { - $pending_meta = $this->decode_order_note_meta($pending); - $pending_coupon = (string) ($pending_meta['coupon_code'] ?? ''); - if (is_array($pending_meta['coupon'] ?? null)) { - $pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon); - } - if ($pending_coupon === $coupon_code_norm) { - $can_resume = true; - } - } - - if ($can_resume) { - $order_number = (string) ($pending['order_number'] ?? ''); - $order = $pending; - } else { - $note = wp_json_encode([ - 'topic_pay' => 1, - 'topic_id' => $topic_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => $base, - 'list_amount' => $list_amount, - 'amount' => $amount, - 'payable' => $amount, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'product_id' => $topic_id, - 'amount' => $amount, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - } - - $order['subject'] = sprintf( - /* translators: %s: topic title */ - __('购买论坛主题「%s」', 'aksrt'), - get_the_title($post) - ); - $order['payment_method'] = $payment_method; - $order['amount'] = $amount; - - if (!class_exists('\AKSRT\Payment\Gateway')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]); - } - - $resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method); - if (empty($resolved['gateway'])) { - return new \WP_Error( - (string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'), - (string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')), - ['status' => 400, 'order_number' => $order_number] - ); - } - - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [ - 'paid_note' => __('用户完成论坛主题购买支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return new \WP_Error( - (string) ($payment_result['code'] ?? 'aksrt_pay_failed'), - (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')), - ['status' => 402, 'order_number' => $order_number] - ); - } - - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => false, - 'poll' => true, - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => true, - ], - ]; - } - - /** - * On paid: unlock topic (or guest cookie) + burn coupon (idempotent). - */ - public function on_order_paid(string $order_number, array $paid_order): void { - $product_type = sanitize_key((string) ($paid_order['product_type'] ?? '')); - if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) { - return; - } - - $topic_id = (int) ($paid_order['product_id'] ?? 0); - $user_id = (int) ($paid_order['user_id'] ?? 0); - if ($topic_id <= 0) { - return; - } - - $meta = $this->decode_order_note_meta($paid_order); - $is_topic_pay = !empty($meta['topic_pay']); - if (!$is_topic_pay) { - $post = get_post($topic_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_topic') { - return; - } - } - - $fulfilled = $user_id <= 0; - if ($user_id > 0) { - $this->mark_unlocked($user_id, $topic_id); - $fulfilled = class_exists('AKSRT\\Forum\\Forum') - ? Forum::get_instance()->has_user_unlocked_topic($user_id, $topic_id) - : in_array($topic_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_unlocked_topics', true)), true); - } elseif (function_exists('aksrt_set_guest_pay_cookies')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (is_array($full)) { - $paid_order = array_merge($full, $paid_order); - } - } - aksrt_set_guest_pay_cookies($paid_order); - } - $this->finalize_topic_order_side_effects($order_number, $paid_order); - if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('付费主题权益已发放。', 'aksrt')] - ); - } - } - - /** - * @return array{post:\WP_Post}|\WP_Error - */ - private function assert_topic(int $topic_id) { - $post = get_post($topic_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('aksrt_invalid', __('主题不存在', 'aksrt'), ['status' => 404]); - } - if ($post->post_status !== 'publish') { - return new \WP_Error('aksrt_invalid', __('主题不可购买', 'aksrt'), ['status' => 404]); - } - - $visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true)); - if ($visibility !== Forum::VISIBILITY_PAY) { - return new \WP_Error('aksrt_not_paid', __('该主题不是付费可见内容', 'aksrt'), ['status' => 400]); - } - - return ['post' => $post]; - } - - /** - * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int} - */ - private function resolve_price(int $topic_id, int $user_id): array { - if (class_exists('AKSRT\\Forum\\Forum')) { - return Forum::get_instance()->resolve_topic_pay_price($topic_id, $user_id); - } - return [ - 'amount' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)), - 'is_vip_price' => false, - 'is_free' => false, - 'label' => '', - 'base' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)), - 'vip_set' => false, - 'vip_level' => 0, - ]; - } - - private function user_has_paid(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($topic_id); - } - if (class_exists('AKSRT\\Forum\\Forum')) { - return Forum::get_instance()->has_user_paid_topic($user_id, $topic_id); - } - if (class_exists('\AKSRT\Payment\Order')) { - return \AKSRT\Payment\Order::get_instance()->has_paid_access( - $user_id, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - $topic_id - ); - } - return false; - } - - private function mark_unlocked(int $user_id, int $topic_id): void { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_topic_unlocked')) { - Forum::get_instance()->mark_topic_unlocked($user_id, $topic_id); - return; - } - if ($user_id <= 0 || $topic_id <= 0) { - return; - } - $unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); - if (!is_array($unlocked)) { - $unlocked = []; - } - $unlocked[] = $topic_id; - update_user_meta($user_id, 'aksrt_unlocked_topics', array_values(array_unique(array_map('intval', $unlocked)))); - } - - private function is_coupon_allowed(int $topic_id): bool { - return (int) get_post_meta($topic_id, '_aksrt_topic_coupon_s', true) === 1; - } - - /** - * VIP list price → coupon → payable. No affiliate rebate on topics. - * - * @param array $priced - * @return array|\WP_Error - */ - private function apply_topic_cash_discounts(array $priced, int $topic_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) { - $list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2); - $pay = $list_amount; - $coupon_discount = 0.0; - $coupon_meta = []; - $coupon_error = ''; - $coupon_code = sanitize_text_field($coupon_code); - - if ($coupon_code !== '') { - if (!$this->is_coupon_allowed($topic_id)) { - $err = new \WP_Error('aksrt_coupon_not_allowed', __('当前主题不支持优惠码。', 'aksrt'), ['status' => 400]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } elseif (!class_exists('\AKSRT\Payment\Coupon')) { - $err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [ - 'user_id' => max(0, $user_id), - 'product_ids' => [$topic_id], - 'context' => 'topic', - ]); - if (empty($result['valid'])) { - $err = new \WP_Error( - 'aksrt_coupon_error', - (string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')), - ['status' => 400] - ); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2); - if ($coupon_discount > $pay) { - $coupon_discount = $pay; - } - $pay = round(max(0.0, $pay - $coupon_discount), 2); - $c = is_array($result['coupon'] ?? null) ? $result['coupon'] : []; - $coupon_meta = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? $coupon_code), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'discount' => $coupon_discount, - ]; - $coupon_code = (string) $coupon_meta['code']; - } - } - } - - return [ - 'list_amount' => $list_amount, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'coupon_code' => $coupon_code, - 'coupon_error' => $coupon_error, - 'payable' => round(max(0.0, $pay), 2), - ]; - } - - /** - * Burn coupon after paid (idempotent via coupon_usage.order_number). - */ - private function finalize_topic_order_side_effects(string $order_number, array $order): void { - $meta = $this->decode_order_note_meta($order); - if (empty($meta['topic_pay'])) { - return; - } - if (($meta['pay_modo'] ?? '') === 'points') { - return; - } - - $buyer_id = (int) ($order['user_id'] ?? 0); - if (function_exists('aksrt_burn_order_coupon')) { - aksrt_burn_order_coupon($order_number, $meta, $buyer_id); - } - } - - /** - * Free VIP or free-after-discount paid order. - * - * @param array{ - * list_amount?:float, - * payable?:float, - * coupon_code?:string, - * coupon_discount?:float, - * coupon?:array, - * message?:string, - * paid_note?:string - * } $opts - * @return array|\WP_Error - */ - private function create_free_paid_order(int $topic_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) { - $list_amount = round((float) ($opts['list_amount'] ?? 0), 2); - $payable = round((float) ($opts['payable'] ?? 0), 2); - $coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? '')); - $coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : []; - $message = (string) ($opts['message'] ?? __('VIP 免费解锁成功', 'aksrt')); - $paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费解锁主题。', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - $this->mark_unlocked($user_id, $topic_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $note = wp_json_encode([ - 'topic_pay' => 1, - 'topic_id' => $topic_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => (float) ($priced['base'] ?? 0), - 'list_amount' => $list_amount, - 'amount' => $payable, - 'payable' => $payable, - 'coupon_code' => $coupon_code, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'free' => 1, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'product_id' => $topic_id, - 'amount' => 0.0, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => 'free', - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - $this->mark_unlocked($user_id, $topic_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'free', - 'payment_transaction_id' => 'free_' . $order_number, - 'note' => $paid_note, - ]); - if (!$paid) { - return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); - } - - $this->mark_unlocked($user_id, $topic_id); - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - private function find_pending_topic_order(int $user_id, int $topic_id): ?array { - if ($user_id <= 0 || $topic_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) { - return null; - } - $orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [ - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'status' => \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING, - 'limit' => 20, - 'offset' => 0, - ]); - foreach ($orders as $order) { - if ((int) ($order['product_id'] ?? 0) !== $topic_id) { - continue; - } - $meta = $this->decode_order_note_meta($order); - if (!empty($meta['topic_pay']) || $this->is_forum_topic_id($topic_id)) { - return $order; - } - } - return null; - } - - private function is_forum_topic_id(int $topic_id): bool { - $post = get_post($topic_id); - return $post instanceof \WP_Post && $post->post_type === 'aksrt_forum_topic'; - } - - private function decode_order_note_meta(array $order): array { - if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) { - return $order['meta_payload']; - } - $note = (string) ($order['note'] ?? ''); - if ($note === '') { - return []; - } - $decoded = json_decode($note, true); - return is_array($decoded) ? $decoded : []; - } -} diff --git a/src/AKSRT/Forum/TopicService.php b/src/AKSRT/Forum/TopicService.php deleted file mode 100644 index 0d13088f..00000000 --- a/src/AKSRT/Forum/TopicService.php +++ /dev/null @@ -1,275 +0,0 @@ -forum = $forum; - } - - // ================================================================ - // Topic CRUD - // ================================================================ - - /** - * Update topic content and editable governance metadata. - * - * @return bool|\WP_Error - */ - public function update_topic(int $topic_id, array $data) { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $update = ['ID' => $topic_id]; - - if (isset($data['title'])) { - $title = sanitize_text_field((string) $data['title']); - if ($title === '') { - return new \WP_Error('empty_title', __('标题不能为空', 'aksrt')); - } - $update['post_title'] = $title; - } - - if (isset($data['content'])) { - $content = wp_kses_post((string) $data['content']); - if ($content === '') { - return new \WP_Error('empty_content', __('内容不能为空', 'aksrt')); - } - $update['post_content'] = $content; - } - - if (isset($data['excerpt'])) { - $update['post_excerpt'] = sanitize_textarea_field((string) $data['excerpt']); - } - - $result = wp_update_post($update, true); - if (is_wp_error($result)) { - return $result; - } - - if (isset($data['tags']) && is_array($data['tags'])) { - $tags = array_values(array_filter(array_map('sanitize_text_field', $data['tags']))); - wp_set_object_terms($topic_id, $tags, 'aksrt_forum_tag'); - } - - $this->save_topic_governance_meta($topic_id, $data); - - return true; - } - - /** - * Create topic. - * - * @return array|\WP_Error - */ - public function create_topic(int $user_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $board_id = (int) ($data['board_id'] ?? 0); - $title = sanitize_text_field((string) ($data['title'] ?? '')); - $content = wp_kses_post((string) ($data['content'] ?? '')); - $visibility = sanitize_key((string) ($data['visibility'] ?? Forum::VISIBILITY_PUBLIC)); - $special_topic = sanitize_text_field((string) ($data['special_topic'] ?? '')); - $tags = is_array($data['tags'] ?? null) ? $data['tags'] : []; - $bbs_type = sanitize_key((string) ($data['bbs_type'] ?? Forum::BBS_TYPE_STANDARD)); - if (!in_array($bbs_type, [Forum::BBS_TYPE_STANDARD, Forum::BBS_TYPE_QUESTION], true)) { - $bbs_type = Forum::BBS_TYPE_STANDARD; - } - - if ($board_id <= 0 || $title === '' || $content === '') { - return new \WP_Error('invalid_payload', __('请填写所有必填项', 'aksrt')); - } - - // §7.3: Strip unauthorized media from content based on compose capabilities - $content = $this->sanitize_topic_content($user_id, $content); - - $board_ok = $this->assert_board($board_id); - if (is_wp_error($board_ok)) { - return $board_ok; - } - - if (!isset($this->get_visibility_map()[$visibility])) { - $visibility = Forum::VISIBILITY_PUBLIC; - } - - $can_publish = $this->can_user_publish_directly($user_id, $board_id); - $post_status = $can_publish ? 'publish' : 'pending'; - $review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING; - - $post_id = wp_insert_post([ - 'post_title' => $title, - 'post_content' => $content, - 'post_status' => $post_status, - 'post_author' => $user_id, - 'post_type' => 'aksrt_forum_topic', - ], true); - - if (is_wp_error($post_id)) { - return $post_id; - } - - $this->attach_topic_to_board((int) $post_id, $board_id); - - $clean_tags = array_values(array_filter(array_map('sanitize_text_field', $tags))); - if (count($clean_tags) > 5) { - $clean_tags = array_slice($clean_tags, 0, 5); - } - if (!empty($clean_tags)) { - wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag'); - } - - $topic_cat = sanitize_text_field((string) ($data['topic_cat'] ?? '')); - if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) { - wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat'); - } - - update_post_meta($post_id, '_aksrt_visibility', $visibility); - update_post_meta($post_id, '_aksrt_review_status', $review_status); - - if ($visibility === Forum::VISIBILITY_CREDITS) { - $credits_cost = max(0, (int) ($data['credits_cost'] ?? 0)); - update_post_meta($post_id, '_aksrt_credits_cost', $credits_cost); - update_post_meta($post_id, '_aksrt_pay_credits', $credits_cost); - if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1'])); - } - if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2'])); - } - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - } - if ($visibility === Forum::VISIBILITY_PAY) { - $pay_price = sanitize_text_field((string) ($data['pay_price'] ?? '')); - update_post_meta($post_id, '_aksrt_pay_price', $pay_price); - if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') { - $v1 = max(0, (float) $data['pay_vip1_price']); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1); - update_post_meta($post_id, '_aksrt_pay_vip_price', $v1); - } - if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price'])); - } - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0); - } - if ($visibility === Forum::VISIBILITY_ROLE) { - $roles = isset($data['visibility_roles']) && is_array($data['visibility_roles']) - ? array_map('sanitize_key', $data['visibility_roles']) : []; - update_post_meta($post_id, '_aksrt_visibility_roles', $roles); - } - - update_post_meta($post_id, '_aksrt_special_topic', $special_topic); - // 置顶分级 0~3(§7.3):新建主题默认不置顶。 - update_post_meta($post_id, '_aksrt_sticky', Forum::STICKY_NONE); - update_post_meta($post_id, '_aksrt_featured', '0'); - update_post_meta($post_id, '_aksrt_locked', '0'); - update_post_meta($post_id, '_aksrt_views', 0); - update_post_meta($post_id, '_aksrt_last_reply', (string) get_post_field('post_date', $post_id)); - update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type); - if ($bbs_type === Forum::BBS_TYPE_QUESTION) { - update_post_meta($post_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED); - } - - $this->save_cover_meta($post_id, $data); - - $vote_meta = $this->prepare_vote_meta($data); - $this->save_vote_meta($post_id, $vote_meta); - - if ($post_status === 'publish') { - $this->forum->sync_board_count($board_id); - } - - if (class_exists('\AKSRT\Credits\Earn') && $post_status === 'publish') { - do_action('aksrt_forum_topic_created', $user_id, $post_id); - } - - if ($post_status === 'pending') { - $this->forum->notify_topic_pending($post_id, $user_id); - } - - return [ - 'post_id' => $post_id, - 'message' => $post_status === 'publish' - ? __('主题已发布', 'aksrt') - : __('主题已提交审核,审核通过后对外展示', 'aksrt'), - 'redirect' => $post_status === 'publish' - ? get_permalink($post_id) - : add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()), - ]; - } - - // ================================================================ - // Board / plate attachment helpers - // ================================================================ - - /** - * Validate board_id as plate CPT post ID when plate is active, else taxonomy term. - * - * @return true|\WP_Error - */ - private function assert_board(int $board_id, string $error_message = '') { - if ($error_message === '') { - $error_message = __('版块不存在', 'aksrt'); - } - - if ($board_id <= 0) { - return new \WP_Error('invalid_board', $error_message); - } - - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($board_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate' || $post->post_status !== 'publish') { - return new \WP_Error('invalid_board', $error_message); - } - return true; - } - - $board = get_term($board_id, 'aksrt_forum_board'); - if (!$board || is_wp_error($board)) { - return new \WP_Error('invalid_board', $error_message); - } - - return true; - } - - /** - * Attach a topic to a board. - * Plate CPT active: store `_aksrt_plate_id` only (native association). - * Otherwise: taxonomy term on aksrt_forum_board. - */ - private function attach_topic_to_board(int $topic_id, int $board_id): void { - if ($topic_id <= 0 || $board_id <= 0) { - return; - } - - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($topic_id, '_aksrt_plate_id', $board_id); - return; - } - - if (taxonomy_exists('aksrt_forum_board')) { - wp_set_object_terms($topic_id, [$board_id], 'aksrt_forum_board'); - } - } - - // ================================================================ - // Topic moderation - // ================================================================ -} diff --git a/src/AKSRT/Forum/TopicServicePart1.php b/src/AKSRT/Forum/TopicServicePart1.php deleted file mode 100644 index 52f9327d..00000000 --- a/src/AKSRT/Forum/TopicServicePart1.php +++ /dev/null @@ -1,441 +0,0 @@ -post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - if (!$this->forum->can_moderate_topic($operator_id, $topic_id)) { - return new \WP_Error('forbidden', __('无权执行该治理动作', 'aksrt')); - } - - $action = sanitize_key($action); - $note = sanitize_textarea_field((string) ($payload['note'] ?? '')); - - switch ($action) { - case 'move_board': - $target_board_id = (int) ($payload['target_board_id'] ?? 0); - $target_ok = $this->assert_board($target_board_id, __('目标版块不存在', 'aksrt')); - if (is_wp_error($target_ok)) { - return $target_ok; - } - - if ($this->forum->is_plate_cpt_active()) { - $previous_plate_id = (int) $this->forum->get_topic_plate_id($topic_id); - $this->attach_topic_to_board($topic_id, $target_board_id); - if ($previous_plate_id > 0 && $previous_plate_id !== $target_board_id) { - $this->forum->sync_board_count($previous_plate_id); - } - $this->forum->sync_board_count($target_board_id); - } else { - $current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - if (is_wp_error($current_board_ids)) { - $current_board_ids = []; - } - $this->attach_topic_to_board($topic_id, $target_board_id); - foreach ($current_board_ids as $current_board_id) { - $this->forum->sync_board_count((int) $current_board_id); - } - $this->forum->sync_board_count($target_board_id); - } - break; - - case 'visibility': - $visibility = sanitize_key((string) ($payload['visibility'] ?? '')); - if (!isset($this->get_visibility_map()[$visibility])) { - return new \WP_Error('invalid_visibility', __('可见性参数无效', 'aksrt')); - } - update_post_meta($topic_id, '_aksrt_visibility', $visibility); - if ($visibility === Forum::VISIBILITY_CREDITS) { - update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($payload['credits_cost'] ?? 0))); - delete_post_meta($topic_id, '_aksrt_pay_price'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } elseif ($visibility === Forum::VISIBILITY_PAY) { - update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($payload['pay_price'] ?? ''))); - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } elseif ($visibility === Forum::VISIBILITY_ROLE) { - $roles = isset($payload['visibility_roles']) && is_array($payload['visibility_roles']) - ? array_values(array_filter(array_map('sanitize_key', $payload['visibility_roles']))) : []; - update_post_meta($topic_id, '_aksrt_visibility_roles', $roles); - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_pay_price'); - } else { - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_pay_price'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } - break; - - case 'review': - $review_status = sanitize_key((string) ($payload['review_status'] ?? '')); - if (!isset($this->get_review_map()[$review_status])) { - return new \WP_Error('invalid_review_status', __('审核状态参数无效', 'aksrt')); - } - - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - update_post_meta($topic_id, '_aksrt_review_status', $review_status); - if ($review_status === Forum::REVIEW_APPROVED) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - } elseif ($review_status === Forum::REVIEW_PENDING) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); - } else { - wp_update_post(['ID' => $topic_id, 'post_status' => 'draft']); - } - - foreach ($board_ids as $board_id) { - $this->forum->sync_board_count((int) $board_id); - } - break; - - case 'sticky': { - $raw = isset($payload['value']) ? $payload['value'] : null; - if ($raw === false || $raw === null) { - $level = Forum::STICKY_NONE; - } elseif (is_numeric($raw)) { - $level = (int) $raw; - } else { - $level = !empty($raw) ? Forum::STICKY_PLATE : Forum::STICKY_NONE; - } - - $level = max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, $level)); - update_post_meta($topic_id, '_aksrt_sticky', $level); - break; - } - - case 'featured': { - $prev = get_post_meta($topic_id, '_aksrt_featured', true) === '1'; - $next = !empty($payload['value']); - update_post_meta($topic_id, '_aksrt_featured', $next ? '1' : '0'); - - // 精华联动:从「未设精华」→「设为精华」时触发积分奖励 + 站内信通知(§7.3/§7.7/§7.8)。 - // 从「精华」→「取消」时不回收积分,仅不重复发放。 - if ($next && !$prev) { - $this->reward_essence($topic_id, $operator_id); - } - break; - } - - case 'locked': { - $locked = !empty($payload['value']); - update_post_meta($topic_id, '_aksrt_locked', $locked ? '1' : '0'); - wp_update_post([ - 'ID' => $topic_id, - 'comment_status' => $locked ? 'closed' : 'open', - ]); - break; - } - - case 'question_status': { - $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); - if ($bbs_type !== Forum::BBS_TYPE_QUESTION) { - return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); - } - $status = (int) ($payload['value'] ?? Forum::QUESTION_UNSOLVED); - $status = in_array($status, [Forum::QUESTION_UNSOLVED, Forum::QUESTION_SOLVED], true) - ? $status : Forum::QUESTION_UNSOLVED; - update_post_meta($topic_id, '_aksrt_question_status', $status); - break; - } - - case 'close': { - $closed = !empty($payload['value']); - if ($closed) { - update_post_meta($topic_id, '_aksrt_closed', '1'); - wp_update_post(['ID' => $topic_id, 'comment_status' => 'closed']); - } else { - delete_post_meta($topic_id, '_aksrt_closed'); - $locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1'; - if (!$locked) { - wp_update_post(['ID' => $topic_id, 'comment_status' => 'open']); - } - } - break; - } - - case 'hide': { - $hidden = !empty($payload['value']); - if ($hidden) { - update_post_meta($topic_id, '_aksrt_hidden', '1'); - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_update_post(['ID' => $topic_id, 'post_status' => 'private']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - } else { - delete_post_meta($topic_id, '_aksrt_hidden'); - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - } - break; - } - - case 'delete': { - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_trash_post($topic_id); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - $author_id = (int) $post->post_author; - if ($author_id > 0 && $author_id !== $operator_id) { - $this->forum->notify($author_id, 'system', sprintf( - __('你的帖子「%s」已被删除', 'aksrt'), - $post->post_title - ), [ - 'title' => __('帖子已删除', 'aksrt'), - 'target_url' => aksrt_get_forum_home_url(), - 'reason' => $note, - ]); - } - break; - } - - case 'undelete': { - if ($post->post_status !== 'trash') { - return new \WP_Error('not_trashed', __('该帖不在回收站中', 'aksrt')); - } - wp_untrash_post($topic_id); - $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); - if ($review_status === Forum::REVIEW_APPROVED) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - } else { - wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); - } - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - break; - } - - default: - return new \WP_Error('invalid_action', __('不支持的治理动作', 'aksrt')); - } - - if ($note !== '') { - update_post_meta($topic_id, '_aksrt_moderation_note', $note); - } - - update_post_meta($topic_id, '_aksrt_moderated_by', $operator_id); - update_post_meta($topic_id, '_aksrt_moderated_at', current_time('mysql')); - - $this->forum->notify_moderation_result($topic_id, $action, $operator_id); - - return [ - 'message' => __('治理动作已执行', 'aksrt'), - 'topic' => $this->build_topic_payload(get_post($topic_id), $operator_id), - ]; - } - - /** - * 精华联动奖励(§7.3 联动 + §7.7 被设精华奖励 + §7.8 精华通知)。 - * - * - 触发积分奖励:读 `credits.essence` 配置,有每日上限与去重 meta(防止重复发放)。 - * - 触发站内信通知作者(依赖 \AKSRT\Messages\Message inbox 子系统)。 - * - 缺失依赖(积分模块未启用/未配置)时静默跳过,不阻塞治理动作。 - */ - public function reward_essence(int $topic_id, int $operator_id): void { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - - $author_id = (int) $post->post_author; - if ($author_id <= 0) { - return; - } - - // 去重:同一帖已发过精华奖励则跳过(取消再加不重复发)。 - if (get_post_meta($topic_id, '_aksrt_essence_rewarded', true) === '1') { - return; - } - - // 积分奖励(§7.7):统一走免费积分日上限。 - if (class_exists('\AKSRT\Settings\Settings')) { - $cfg = \AKSRT\Settings\Settings::get_forum_settings(); - $amount = (int) ($cfg['credits']['essence'] ?? 0); - if ($amount > 0) { - $granted = 0; - if (class_exists('\AKSRT\Credits\PointsFree')) { - $result = \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_essence', $amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - $granted = is_wp_error($result) ? 0 : (int) $result; - } elseif (class_exists('\AKSRT\Credits\Credits')) { - $daily_max = (int) ($cfg['credits']['daily_max'] ?? 50); - if ($daily_max > 0 && $this->today_credits_count($author_id, 'forum_essence') >= $daily_max) { - $amount = 0; - } - if ($amount > 0) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_essence', [ - 'type' => 'post', - 'id' => $topic_id, - ], __('主题被设为精华', 'aksrt')); - $granted = $amount; - } - } - if ($granted > 0) { - update_post_meta($topic_id, '_aksrt_essence_rewarded', '1'); - update_post_meta($topic_id, '_aksrt_essence_rewarded_at', current_time('mysql')); - } - } - } - - // 站内信通知作者(§7.8)。 - if (class_exists('\AKSRT\Messages\Message')) { - $title = get_the_title($topic_id); - $permalink = (string) get_permalink($topic_id); - \AKSRT\Messages\Message::get_instance()->send( - $author_id, - 'forum_essence', - sprintf(__('你的主题「%s」被设为精华', 'aksrt'), $title), - [ - 'title' => __('主题被设为精华', 'aksrt'), - 'target_url' => $permalink, - ] - ); - } - - do_action('aksrt_forum_topic_featured', $topic_id, $author_id, $operator_id); - } - - /** - * 今日某 reason 的积分发放次数(用于精华/被加分等每日上限判断)。 - */ - public function today_credits_count(int $user_id, string $reason): int { - if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) { - return 0; - } - - $table = \AKSRT\Core\DBMigrator::table('credit_log'); - if (!$table) { - return 0; - } - - global $wpdb; - $today = current_time('Y-m-d'); - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s", - $user_id, $reason, $today - )); - } - - /** - * 置顶分级 label(对标 zibll zib_bbs_get_topping_badge)。 - */ - public function get_sticky_label(int $level): string { - switch ($level) { - case Forum::STICKY_GLOBAL: - return __('全局置顶', 'aksrt'); - case Forum::STICKY_SUPER: - return __('超级置顶', 'aksrt'); - case Forum::STICKY_PLATE: - return __('置顶', 'aksrt'); - default: - return ''; - } - } - - /** - * 读取并规范化置顶分级 meta。 - * - * 兼容历史 0/1 二态 meta("1" → STICKY_PLATE),新数据存 0~3 整数。 - */ - public function resolve_sticky_level(int $topic_id): int { - $raw = get_post_meta($topic_id, '_aksrt_sticky', true); - if ($raw === '' || $raw === null) { - return Forum::STICKY_NONE; - } - - if (is_numeric($raw)) { - return max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, (int) $raw)); - } - - return $raw === '1' ? Forum::STICKY_PLATE : Forum::STICKY_NONE; - } - - // ================================================================ - // Voting system - // ================================================================ - - /** - * Submit vote. - * - * @return array|\WP_Error - */ - public function submit_vote(int $topic_id, int $user_id, array $option_ids) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录后投票', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $vote = $this->get_vote_payload($topic_id, $user_id); - if (empty($vote['enabled'])) { - return new \WP_Error('vote_disabled', __('当前主题未开启投票', 'aksrt')); - } - - if (!empty($vote['expired'])) { - return new \WP_Error('vote_expired', __('投票已结束', 'aksrt')); - } - - if (!empty($vote['has_voted'])) { - return new \WP_Error('already_voted', __('你已经参与过本次投票', 'aksrt')); - } - - $selected = array_values(array_unique(array_filter(array_map('sanitize_text_field', $option_ids)))); - if (empty($selected)) { - return new \WP_Error('empty_selection', __('请选择至少一个投票选项', 'aksrt')); - } - - if (empty($vote['multiple']) && count($selected) > 1) { - return new \WP_Error('too_many_selection', __('当前投票仅支持单选', 'aksrt')); - } - - $options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $options = is_array($options) ? $options : []; - $valid_option_ids = wp_list_pluck($options, 'id'); - - foreach ($selected as $selected_id) { - if (!in_array($selected_id, $valid_option_ids, true)) { - return new \WP_Error('invalid_option', __('投票选项不存在', 'aksrt')); - } - } - - $records = get_post_meta($topic_id, '_aksrt_vote_records', true); - $records = is_array($records) ? $records : []; - $records[(string) $user_id] = $selected; - update_post_meta($topic_id, '_aksrt_vote_records', $records); - - foreach ($options as &$option) { - if (in_array((string) ($option['id'] ?? ''), $selected, true)) { - $option['count'] = max(0, (int) ($option['count'] ?? 0)) + 1; - } - } - unset($option); - - update_post_meta($topic_id, '_aksrt_vote_options', $options); - - return [ - 'message' => __('投票成功', 'aksrt'), - 'vote' => $this->get_vote_payload($topic_id, $user_id), - ]; - } -} diff --git a/src/AKSRT/Forum/TopicServicePart2.php b/src/AKSRT/Forum/TopicServicePart2.php deleted file mode 100644 index a6e285a9..00000000 --- a/src/AKSRT/Forum/TopicServicePart2.php +++ /dev/null @@ -1,382 +0,0 @@ -forum->get_config(); - $vote_max = (int) ($config['other']['vote_max'] ?? 8); - if ($vote_max > 0 && count($labels) > $vote_max) { - $labels = array_slice($labels, 0, $vote_max); - } - - $vote_type = sanitize_key((string) ($data['vote_type'] ?? 'standard')); - if ($vote_type === 'pk' && count($labels) !== 2) { - $vote_type = 'standard'; - } - if ($vote_type === 'pk') { - $multiple = false; - } - - if (count($labels) < 2) { - $enabled = false; - } - - $expiry_days = (int) ($data['vote_expiry_days'] ?? 0); - - $options = []; - foreach ($labels as $index => $label) { - $options[] = [ - 'id' => 'option_' . ($index + 1), - 'label' => $label, - 'count' => 0, - ]; - } - - return [ - 'enabled' => $enabled, - 'title' => $title, - 'multiple' => $multiple, - 'vote_type' => $vote_type, - 'expiry_days' => $expiry_days, - 'options' => $options, - ]; - } - - /** - * Save vote meta while preserving counts when possible. - */ - public function save_vote_meta(int $topic_id, array $vote_meta): void { - $existing_options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $existing_options = is_array($existing_options) ? $existing_options : []; - $existing_by_label = []; - foreach ($existing_options as $existing_option) { - if (!empty($existing_option['label'])) { - $existing_by_label[(string) $existing_option['label']] = $existing_option; - } - } - - $options = []; - foreach ($vote_meta['options'] as $index => $option) { - $label = (string) ($option['label'] ?? ''); - if ($label === '') { - continue; - } - - $existing = $existing_by_label[$label] ?? null; - $options[] = [ - 'id' => $existing['id'] ?? ('option_' . ($index + 1)), - 'label' => $label, - 'count' => (int) ($existing['count'] ?? 0), - ]; - } - - update_post_meta($topic_id, '_aksrt_vote_enabled', !empty($vote_meta['enabled']) ? '1' : '0'); - update_post_meta($topic_id, '_aksrt_vote_title', sanitize_text_field((string) ($vote_meta['title'] ?? ''))); - update_post_meta($topic_id, '_aksrt_vote_multiple', !empty($vote_meta['multiple']) ? '1' : '0'); - update_post_meta($topic_id, '_aksrt_vote_options', $options); - $vote_type = in_array($vote_meta['vote_type'] ?? '', ['standard', 'pk'], true) ? $vote_meta['vote_type'] : 'standard'; - update_post_meta($topic_id, '_aksrt_vote_type', $vote_type); - $expiry_days = (int) ($vote_meta['expiry_days'] ?? 0); - if (!in_array($expiry_days, [0, 1, 7, 30], true)) { - $expiry_days = 0; - } - update_post_meta($topic_id, '_aksrt_vote_expiry_days', $expiry_days); - if ($expiry_days > 0 && !get_post_meta($topic_id, '_aksrt_vote_started_at', true)) { - update_post_meta($topic_id, '_aksrt_vote_started_at', current_time('mysql')); - } - - if (empty($vote_meta['enabled'])) { - delete_post_meta($topic_id, '_aksrt_vote_records'); - } - } - - /** - * Get vote payload for a topic. - */ - public function get_vote_payload(int $topic_id, int $user_id = 0): array { - $enabled = get_post_meta($topic_id, '_aksrt_vote_enabled', true) === '1'; - $options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $options = is_array($options) ? $options : []; - $records = get_post_meta($topic_id, '_aksrt_vote_records', true); - $records = is_array($records) ? $records : []; - - $vote_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_vote_type', true)); - if (!in_array($vote_type, ['standard', 'pk'], true)) { - $vote_type = 'standard'; - } - $expiry_days = (int) get_post_meta($topic_id, '_aksrt_vote_expiry_days', true); - $started_at = (string) get_post_meta($topic_id, '_aksrt_vote_started_at', true); - $expired = false; - if ($expiry_days > 0 && $started_at !== '') { - $expire_time = strtotime($started_at) + ($expiry_days * DAY_IN_SECONDS); - $expired = time() >= $expire_time; - } - - $config = $this->forum->get_config(); - $display_mode = $config['other']['vote_number_type'] ?? 'percent'; - - $selected = $user_id > 0 && isset($records[(string) $user_id]) && is_array($records[(string) $user_id]) - ? array_values(array_map('strval', $records[(string) $user_id])) - : []; - $total_votes = 0; - - foreach ($options as &$option) { - $count = max(0, (int) ($option['count'] ?? 0)); - $option['count'] = $count; - $option['selected'] = in_array((string) ($option['id'] ?? ''), $selected, true); - $total_votes += $count; - } - unset($option); - - return [ - 'enabled' => $enabled && !empty($options), - 'title' => (string) get_post_meta($topic_id, '_aksrt_vote_title', true), - 'multiple' => get_post_meta($topic_id, '_aksrt_vote_multiple', true) === '1', - 'vote_type' => $vote_type, - 'expiry_days' => $expiry_days, - 'expired' => $expired, - 'display_mode' => $display_mode, - 'options' => $options, - 'total_votes' => $total_votes, - 'has_voted' => !empty($selected), - 'selected' => $selected, - ]; - } - - // ================================================================ - // Content processing - // ================================================================ - - /** - * Save governance meta fields from an editable payload. - */ - public function save_topic_governance_meta(int $topic_id, array $data): void { - if (isset($data['visibility'])) { - $visibility = sanitize_key((string) $data['visibility']); - if (isset($this->get_visibility_map()[$visibility])) { - update_post_meta($topic_id, '_aksrt_visibility', $visibility); - } - } - - if (array_key_exists('special_topic', $data)) { - update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $data['special_topic'])); - } - - if (array_key_exists('vote_enabled', $data) || array_key_exists('vote_options', $data) || array_key_exists('vote_options_text', $data)) { - $this->save_vote_meta($topic_id, $this->prepare_vote_meta($data)); - } - } - - public function save_cover_meta(int $post_id, array $data): void { - $cover_type = sanitize_key((string) ($data['cover_type'] ?? '')); - if (!in_array($cover_type, ['image', 'slide', 'video'], true)) { - delete_post_meta($post_id, '_aksrt_cover_type'); - return; - } - - update_post_meta($post_id, '_aksrt_cover_type', $cover_type); - - if ($cover_type === 'image') { - $url = esc_url_raw((string) ($data['cover_image'] ?? '')); - update_post_meta($post_id, '_aksrt_cover_image', $url); - } elseif ($cover_type === 'slide') { - $raw = $data['featured_slide'] ?? ''; - $slides = is_array($raw) ? $raw : json_decode((string) $raw, true); - if (!is_array($slides)) { - $slides = []; - } - $slides = array_values(array_filter(array_map('esc_url_raw', array_slice($slides, 0, 9)))); - update_post_meta($post_id, '_aksrt_featured_slide', $slides); - } elseif ($cover_type === 'video') { - $url = esc_url_raw((string) ($data['featured_video'] ?? '')); - update_post_meta($post_id, '_aksrt_featured_video', $url); - } - } - - /** - * Sanitize topic content based on user's compose capabilities. - * - * Strips ,