用户功能设置收口与废弃后台链路清理
This commit is contained in:
@@ -23,6 +23,8 @@
|
|||||||
!/tests/widget-system/page-display-save-harness.php
|
!/tests/widget-system/page-display-save-harness.php
|
||||||
!/tests/widget-system/admin-ui-contract.test.cjs
|
!/tests/widget-system/admin-ui-contract.test.cjs
|
||||||
!/tests/widget-system/auth-source-contract.test.cjs
|
!/tests/widget-system/auth-source-contract.test.cjs
|
||||||
|
!/tests/widget-system/user-badges-contract.test.cjs
|
||||||
|
!/tests/widget-system/user-features-contract.test.cjs
|
||||||
!/tests/visual/docker-compose.zibll.yml
|
!/tests/visual/docker-compose.zibll.yml
|
||||||
!/tests/visual/zibll-Caddyfile
|
!/tests/visual/zibll-Caddyfile
|
||||||
!/tests/visual/README.md
|
!/tests/visual/README.md
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ AKSRT 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题
|
|||||||
- **支付/下载主线** — 下载/付费内容中心、充值/VIP/分成/返佣/网关结构、10 分钟下载 token
|
- **支付/下载主线** — 下载/付费内容中心、充值/VIP/分成/返佣/网关结构、10 分钟下载 token
|
||||||
- **用户与增长** — 统一登录注册、OAuth 登录、签到奖励、邀请奖励、等级/徽章/认证/举报结构
|
- **用户与增长** — 统一登录注册、OAuth 登录、签到奖励、邀请奖励、等级/徽章/认证/举报结构
|
||||||
- **内容与交互** — 首页模块化、归档/搜索/404、单篇资源页、评论审核与通知中心
|
- **内容与交互** — 首页模块化、归档/搜索/404、单篇资源页、评论审核与通知中心
|
||||||
- **运维工具** — 配置导出导入、dry-run 校验、邮件回滚、数据库与环境诊断
|
- **运维工具** — 配置导出导入、dry-run 校验、备份、数据库与环境诊断
|
||||||
- **自定义页面模板**:About / Friends / Login / Profile / Register
|
- **自定义页面模板**:About / Friends / Login / Profile / Register
|
||||||
- **响应式设计**
|
- **响应式设计**
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
|
|||||||
- `功能&权限`:审核、验证码、评论/附件/下载权限
|
- `功能&权限`:审核、验证码、评论/附件/下载权限
|
||||||
- `用户&互动`:登录注册、OAuth、通知、签到、认证、等级、徽章、举报、积分
|
- `用户&互动`:登录注册、OAuth、通知、签到、认证、等级、徽章、举报、积分
|
||||||
- `支付&付费`:下载/付费内容中心、余额充值、VIP、创作分成、推广返佣、收款接口
|
- `支付&付费`:下载/付费内容中心、余额充值、VIP、创作分成、推广返佣、收款接口
|
||||||
- `备份&导入`:导入导出、健康检查、迁移状态、邮件回滚
|
- `备份&导入`:导入导出、健康检查、迁移状态、备份与更新
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export const toolsApiMethods = {
|
|||||||
detectUpdate() { return this.request('/tools/update-check', { method: 'POST', body: JSON.stringify({}) }) },
|
detectUpdate() { return this.request('/tools/update-check', { method: 'POST', body: JSON.stringify({}) }) },
|
||||||
dismissUpdate(version) { return this.request('/tools/update-dismiss', { method: 'POST', body: JSON.stringify({ version }) }) },
|
dismissUpdate(version) { return this.request('/tools/update-dismiss', { method: 'POST', body: JSON.stringify({ version }) }) },
|
||||||
|
|
||||||
getEmailRollbackUsers() { return this.request('/email-rollback/users') },
|
|
||||||
executeEmailRollback(id) { return this.request(`/email-rollback/users/${id}/rollback`, { method: 'POST' }) },
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Settings Management (Import / Export / Reset / Health / Schema)
|
* Settings Management (Import / Export / Reset / Health / Schema)
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|||||||
@@ -36,15 +36,6 @@ export const extensionsSection = {
|
|||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'extensions-tools',
|
|
||||||
name: 'ExtensionsTools',
|
|
||||||
label: '系统工具',
|
|
||||||
route: '/extensions/tools',
|
|
||||||
component: 'ExtensionTools',
|
|
||||||
title: '系统工具',
|
|
||||||
desc: '额外运维工具与扩展能力。',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'extensions-ai',
|
key: 'extensions-ai',
|
||||||
name: 'ExtensionAI',
|
name: 'ExtensionAI',
|
||||||
|
|||||||
@@ -25,16 +25,6 @@ export const toolsSection = {
|
|||||||
canonicalDomain: 'tools',
|
canonicalDomain: 'tools',
|
||||||
section: 'backup',
|
section: 'backup',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'tools-frontend-modal-rollout',
|
|
||||||
name: 'ToolsFrontendModalRollout',
|
|
||||||
label: '前台弹窗灰度',
|
|
||||||
route: '/tools/frontend-modal-rollout',
|
|
||||||
title: '前台弹窗灰度',
|
|
||||||
desc: '按领域控制前台弹窗新流程的分阶段发布。',
|
|
||||||
canonicalDomain: 'tools',
|
|
||||||
section: 'frontend-modal-rollout',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'tools-health',
|
key: 'tools-health',
|
||||||
name: 'ToolsHealth',
|
name: 'ToolsHealth',
|
||||||
@@ -44,15 +34,6 @@ export const toolsSection = {
|
|||||||
desc: '查看主题环境、数据表和系统诊断状态。',
|
desc: '查看主题环境、数据表和系统诊断状态。',
|
||||||
component: 'ToolsHealth',
|
component: 'ToolsHealth',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'tools-email-rollback',
|
|
||||||
name: 'ToolsEmailRollback',
|
|
||||||
label: '邮件回滚',
|
|
||||||
route: '/tools/email-rollback',
|
|
||||||
title: '邮件回滚',
|
|
||||||
desc: '查看 14 天邮箱变更窗口内的账号并执行回滚。',
|
|
||||||
component: 'ToolsEmailRollback',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'tools-update',
|
key: 'tools-update',
|
||||||
name: 'ToolsUpdate',
|
name: 'ToolsUpdate',
|
||||||
|
|||||||
@@ -30,28 +30,16 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'oauth',
|
section: 'oauth',
|
||||||
}),
|
}),
|
||||||
customPage({
|
schemaPage({
|
||||||
key: 'users-features',
|
key: 'users-features',
|
||||||
name: 'UsersFeatures',
|
name: 'UsersFeatures',
|
||||||
label: '用户功能',
|
label: '用户功能',
|
||||||
route: '/users/features',
|
route: '/users/features',
|
||||||
component: 'UsersFeatures',
|
|
||||||
title: '用户功能',
|
title: '用户功能',
|
||||||
desc: '用户中心与个人主页模块配置。',
|
desc: '配置用户头像、封面和默认签名。',
|
||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'features',
|
section: 'features',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'users-signin',
|
|
||||||
name: 'UsersSignin',
|
|
||||||
label: '签到奖励',
|
|
||||||
route: '/users/signin',
|
|
||||||
component: 'UsersSignin',
|
|
||||||
title: '签到奖励',
|
|
||||||
desc: '签到规则、连续签到奖励和签到页面配置。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'signin',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-credits',
|
key: 'users-credits',
|
||||||
name: 'UsersCredits',
|
name: 'UsersCredits',
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
<div class="aksrt-docs__action">
|
<div class="aksrt-docs__action">
|
||||||
<FaIcon icon="fa fas fa-wrench" :size="16" />
|
<FaIcon icon="fa fas fa-wrench" :size="16" />
|
||||||
<div>
|
<div>
|
||||||
<strong>系统工具</strong>
|
<strong>工具</strong>
|
||||||
<p>导入导出、健康检查、迁移状态</p>
|
<p>导入导出、健康检查、备份与更新</p>
|
||||||
<router-link to="/tools/import-export">前往配置 →</router-link>
|
<router-link to="/tools/import-export">前往配置 →</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="系统工具" desc="额外运维工具与扩展能力。" />
|
|
||||||
|
|
||||||
<!-- Health Overview -->
|
|
||||||
<n-card title="系统健康" size="small" class="aksrt-tools__section">
|
|
||||||
<n-grid :cols="4" :x-gap="12" :y-gap="12">
|
|
||||||
<n-grid-item v-for="item in healthItems" :key="item.key">
|
|
||||||
<div class="aksrt-tools__health-card" :class="'aksrt-tools__health-card--' + item.status">
|
|
||||||
<FaIcon :icon="item.icon" :size="18" :color="item.color" />
|
|
||||||
<div>
|
|
||||||
<strong>{{ item.value }}</strong>
|
|
||||||
<p>{{ item.label }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Tool Categories -->
|
|
||||||
<n-card title="工具集合" size="small" class="aksrt-tools__section">
|
|
||||||
<n-grid :cols="3" :x-gap="12" :y-gap="12">
|
|
||||||
<n-grid-item v-for="tool in tools" :key="tool.key">
|
|
||||||
<div class="aksrt-tools__tool-card" @click="navigateTo(tool.route)">
|
|
||||||
<div class="aksrt-tools__tool-icon" :style="{ background: tool.bg }">
|
|
||||||
<FaIcon :icon="tool.icon" :size="18" color="#fff" />
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-tools__tool-body">
|
|
||||||
<strong>{{ tool.title }}</strong>
|
|
||||||
<p>{{ tool.desc }}</p>
|
|
||||||
</div>
|
|
||||||
<FaIcon icon="fa fas fa-chevron-right" :size="12" color="#c0c4cc" />
|
|
||||||
</div>
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Service Status -->
|
|
||||||
<n-card title="服务状态" size="small" class="aksrt-tools__section">
|
|
||||||
<n-list bordered size="small">
|
|
||||||
<n-list-item v-for="svc in services" :key="svc.key">
|
|
||||||
<n-thing :title="svc.name" :description="svc.description">
|
|
||||||
<template #avatar>
|
|
||||||
<FaIcon :icon="svc.icon" :size="16" :color="svc.color" />
|
|
||||||
</template>
|
|
||||||
<template #header-extra>
|
|
||||||
<n-tag :type="svc.available ? 'success' : 'warning'" size="small">
|
|
||||||
{{ svc.available ? '可用' : '未配置' }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Quick Maintenance -->
|
|
||||||
<n-card title="手动维护" size="small" class="aksrt-tools__section">
|
|
||||||
<n-space>
|
|
||||||
<n-button @click="handleFlushCache">
|
|
||||||
<template #icon><FaIcon icon="fa fas fa-magic" :size="12" /></template>
|
|
||||||
清除缓存
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="handleOptimizeDb">
|
|
||||||
<template #icon><FaIcon icon="fa fas fa-database" :size="12" /></template>
|
|
||||||
优化数据表
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="handleRebuildIndex">
|
|
||||||
<template #icon><FaIcon icon="fa fas fa-sync-alt" :size="12" /></template>
|
|
||||||
重建索引
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
import FaIcon from '@/components/form/FaIcon.vue'
|
|
||||||
import api from '@/api/index.js'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const healthItems = ref([
|
|
||||||
{ key: 'php', icon: 'fa fas fa-code', color: 'var(--aksrt-color-primary)', value: '—', label: 'PHP 版本', status: 'ok' },
|
|
||||||
{ key: 'wp', icon: 'fa fab fa-wordpress', color: '#722ed1', value: '—', label: 'WordPress', status: 'ok' },
|
|
||||||
{ key: 'db', icon: 'fa fas fa-database', color: '#52c41a', value: '—', label: '数据库', status: 'ok' },
|
|
||||||
{ key: 'uploads', icon: 'fa fas fa-folder-open', color: '#fa8c16', value: '—', label: '上传目录', status: 'ok' },
|
|
||||||
])
|
|
||||||
|
|
||||||
const tools = ref([
|
|
||||||
{ key: 'import-export', icon: 'fa fas fa-exchange-alt', bg: 'var(--aksrt-color-primary)', title: '导入导出', desc: '设置导入、导出和重置', route: '/tools/import-export' },
|
|
||||||
{ key: 'health', icon: 'fa fas fa-heartbeat', bg: '#52c41a', title: '健康检查', desc: '系统状态检测和诊断', route: '/tools/health' },
|
|
||||||
{ key: 'email-rollback', icon: 'fa fas fa-undo', bg: '#fa8c16', title: '邮箱回滚', desc: '邮箱变更 14 天内回滚', route: '/tools/email-rollback' },
|
|
||||||
])
|
|
||||||
|
|
||||||
const services = ref([
|
|
||||||
{ key: 'sms', icon: 'fa fas fa-comment', color: 'var(--aksrt-color-primary)', name: '短信服务', description: '验证码和通知短信', available: false },
|
|
||||||
{ key: 'wechat', icon: 'fa fab fa-weixin', color: '#52c41a', name: '微信通知', description: '微信模板消息和订阅通知', available: false },
|
|
||||||
{ key: 'captcha', icon: 'fa fas fa-shield-alt', color: '#722ed1', name: '验证码服务', description: '人机验证(极验/腾讯/阿里云)', available: false },
|
|
||||||
{ key: 'push', icon: 'fa fas fa-paper-plane', color: '#fa8c16', name: '搜索引擎推送', description: '百度/Google 主动推送', available: false },
|
|
||||||
{ key: 'audit', icon: 'fa fas fa-shield-alt', color: '#f5222d', name: '内容审核', description: '第三方内容安全审核 API', available: false },
|
|
||||||
])
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.request('/tools/health')
|
|
||||||
if (res?.data) {
|
|
||||||
const d = res.data
|
|
||||||
if (d.php_version) healthItems.value[0].value = d.php_version
|
|
||||||
if (d.wp_version) healthItems.value[1].value = d.wp_version
|
|
||||||
if (d.db_status) {
|
|
||||||
healthItems.value[2].value = d.db_status
|
|
||||||
healthItems.value[2].status = d.db_ok ? 'ok' : 'warn'
|
|
||||||
}
|
|
||||||
if (d.upload_dir) {
|
|
||||||
healthItems.value[3].value = d.upload_dir
|
|
||||||
healthItems.value[3].status = d.upload_ok ? 'ok' : 'warn'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Endpoint may not exist yet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check service availability from structured status
|
|
||||||
try {
|
|
||||||
const settingsRes = await api.request('/settings/extensions')
|
|
||||||
if (settingsRes?.data) {
|
|
||||||
const s = settingsRes.data
|
|
||||||
// Prefer structured services if available
|
|
||||||
const svc = settingsRes.services || {}
|
|
||||||
services.value[0].available = !!(svc.sms?.available ?? s.sms_enabled)
|
|
||||||
services.value[0].status = svc.sms?.provider || (s.sms_enabled ? '已启用' : '未配置')
|
|
||||||
services.value[1].available = !!(svc.wechat?.available ?? s.wechat_enabled)
|
|
||||||
services.value[1].status = svc.wechat?.provider || (s.wechat_enabled ? '已启用' : '未配置')
|
|
||||||
services.value[2].available = !!(svc.captcha?.available ?? svc.captcha?.configured)
|
|
||||||
services.value[2].status = svc.captcha?.provider || (svc.captcha?.enabled ? '已启用' : '未配置')
|
|
||||||
services.value[3].available = !!(svc.push?.available ?? s.baidu_push_enabled)
|
|
||||||
services.value[3].status = svc.push?.provider || (s.baidu_push_enabled ? '已启用' : '未配置')
|
|
||||||
services.value[4].available = !!(svc.audit?.available ?? s.content_audit_enabled)
|
|
||||||
services.value[4].status = svc.audit?.provider || (s.content_audit_enabled ? '已启用' : '未配置')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Use defaults
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function navigateTo(route) {
|
|
||||||
router.push(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFlushCache() {
|
|
||||||
message.info('正在清除缓存...')
|
|
||||||
try {
|
|
||||||
const res = await api.request('/tools/maintenance', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ action: 'flush_cache' }),
|
|
||||||
})
|
|
||||||
message.success(res?.message || '缓存已清除')
|
|
||||||
} catch (err) {
|
|
||||||
message.error(err?.message || '清除缓存失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleOptimizeDb() {
|
|
||||||
message.info('正在优化数据表...')
|
|
||||||
try {
|
|
||||||
const res = await api.request('/tools/maintenance', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ action: 'optimize_db' }),
|
|
||||||
})
|
|
||||||
message.success(res?.message || '数据表已优化')
|
|
||||||
} catch (err) {
|
|
||||||
message.error(err?.message || '优化数据表失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRebuildIndex() {
|
|
||||||
message.info('正在重建索引...')
|
|
||||||
try {
|
|
||||||
const res = await api.request('/tools/maintenance', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ action: 'rebuild_index' }),
|
|
||||||
})
|
|
||||||
message.success(res?.message || '索引重建完成')
|
|
||||||
} catch (err) {
|
|
||||||
message.error(err?.message || '重建索引失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-tools__section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__health-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__health-card strong {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--aksrt-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__health-card p {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--aksrt-color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__health-card--warn {
|
|
||||||
border-color: #faad14;
|
|
||||||
background: #fffbe6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-card:hover {
|
|
||||||
border-color: var(--aksrt-color-primary);
|
|
||||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-body strong {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--aksrt-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-tools__tool-body p {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--aksrt-color-muted);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader
|
|
||||||
title="邮件回滚"
|
|
||||||
desc="查看 14 天邮箱变更窗口内的账号,并在必要时执行回滚。"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<n-space>
|
|
||||||
<n-button quaternary @click="go('/tools/health')">健康检查</n-button>
|
|
||||||
<n-button quaternary @click="load">刷新列表</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>待处理账号</h3>
|
|
||||||
<n-tag round type="info">总计 {{ total }}</n-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="items.length" class="aksrt-rollback-list">
|
|
||||||
<div v-for="item in items" :key="item.user_id" class="aksrt-rollback-list__item">
|
|
||||||
<div>
|
|
||||||
<strong>{{ item.display_name || item.login }}</strong>
|
|
||||||
<p style="margin: 6px 0 0; color: #4b5565;">
|
|
||||||
{{ item.current_email }} → {{ item.previous_email }}
|
|
||||||
</p>
|
|
||||||
<p style="margin: 6px 0 0; color: var(--aksrt-color-muted); font-size: 12px;">
|
|
||||||
剩余窗口:{{ Math.max(0, Math.floor((item.expires_in || 0) / 3600)) }} 小时
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<n-button size="small" :loading="runningId === item.user_id" @click="handleRollback(item.user_id)">
|
|
||||||
执行回滚
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<n-empty v-else description="当前没有可回滚的邮箱变更记录" />
|
|
||||||
</n-card>
|
|
||||||
</n-spin>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import api from '@/api'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const loading = ref(true)
|
|
||||||
const runningId = ref(0)
|
|
||||||
const items = ref([])
|
|
||||||
const total = ref(0)
|
|
||||||
|
|
||||||
function go(path) {
|
|
||||||
router.push(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractData(response) {
|
|
||||||
return response?.data ?? response ?? {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = extractData(await api.getEmailRollbackUsers())
|
|
||||||
items.value = Array.isArray(data.items) ? data.items : []
|
|
||||||
total.value = Number(data.total || 0)
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '加载邮件回滚列表失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRollback(userId) {
|
|
||||||
runningId.value = userId
|
|
||||||
try {
|
|
||||||
const result = await api.executeEmailRollback(userId)
|
|
||||||
if (result.code === 'success') {
|
|
||||||
message.success(result.message || '回滚成功')
|
|
||||||
await load()
|
|
||||||
} else {
|
|
||||||
message.error(result.message || '回滚失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '回滚失败')
|
|
||||||
} finally {
|
|
||||||
runningId.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-rollback-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-rollback-list__item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border: 1px solid #e7ebf3;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.aksrt-rollback-list__item {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button quaternary @click="go('/tools/import-export')">导入导出</n-button>
|
<n-button quaternary @click="go('/tools/import-export')">导入导出</n-button>
|
||||||
<n-button quaternary @click="go('/tools/email-rollback')">邮件回滚</n-button>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<n-button :loading="resetting" @click="handleReset">重置所选域</n-button>
|
<n-button :loading="resetting" @click="handleReset">重置所选域</n-button>
|
||||||
<n-button type="error" tertiary :loading="resettingAll" @click="handleResetAll">重置全部</n-button>
|
<n-button type="error" tertiary :loading="resettingAll" @click="handleResetAll">重置全部</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<p class="aksrt-tools-hint">仅支持重置 schema 托管的业务域。健康检查、迁移状态和邮件回滚属于运维面板,不参与按域回滚。</p>
|
<p class="aksrt-tools-hint">仅支持重置 schema 托管的业务域。健康检查和迁移状态属于运维面板,不参与按域回滚。</p>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="aksrt-section-card">
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const guides = [
|
|||||||
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
|
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
|
||||||
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/ip' },
|
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/ip' },
|
||||||
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
|
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
|
||||||
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与推广奖励', url: '#/users/signin' },
|
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与推广奖励', url: '#/users/credits' },
|
||||||
{ key: 'badge', title: '等级与徽章', desc: '维护等级、认证与徽章体系', url: '#/users/badge' },
|
{ key: 'badge', title: '等级与徽章', desc: '维护等级、认证与徽章体系', url: '#/users/badge' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="用户功能" desc="用户中心的资料、背景图与公开资料均使用统一的能力矩阵和隐私规则。">
|
|
||||||
<template #actions>
|
|
||||||
<n-space>
|
|
||||||
<n-button quaternary @click="go('/permissions/capabilities')">基本权限</n-button>
|
|
||||||
<n-button quaternary @click="go('/page-display/user-home')">公开主页</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 180px;">
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header"><h3>固定资料与操作入口</h3></div>
|
|
||||||
<n-space vertical size="large">
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
个人资料固定为昵称、个人签名、隐私设置、性别、个人网站、QQ、微信和 GitHub。公开资料由访问者身份过滤,原始社交资料不会下发给无权访客。
|
|
||||||
</n-alert>
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
背景图操作入口始终显示在资料卡右下角的三点菜单中;“上传头像”和“上传用户资料卡背景图”由基本权限矩阵控制。
|
|
||||||
</n-alert>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header"><h3>用户中心面板</h3></div>
|
|
||||||
<n-form label-placement="left" label-width="170" class="aksrt-form">
|
|
||||||
<n-form-item label="可切换业务面板">
|
|
||||||
<n-checkbox-group v-model:value="form.dynamicTabs">
|
|
||||||
<n-space vertical>
|
|
||||||
<n-checkbox value="posts">我的文章</n-checkbox>
|
|
||||||
<n-checkbox value="comments">我的评论</n-checkbox>
|
|
||||||
<n-checkbox value="favorites">我的收藏</n-checkbox>
|
|
||||||
<n-checkbox value="credits">账户余额 / 签到</n-checkbox>
|
|
||||||
</n-space>
|
|
||||||
</n-checkbox-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
|
||||||
</div>
|
|
||||||
</n-spin>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import api from '@/api'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
|
||||||
const form = reactive({ dynamicTabs: ['posts', 'comments', 'favorites'] })
|
|
||||||
|
|
||||||
function go(path) { router.push(path) }
|
|
||||||
function extractData(response) { return response?.data ?? response ?? {} }
|
|
||||||
function parseTabs(value) {
|
|
||||||
const values = Array.isArray(value) ? value : String(value || '').split(/[\s,]+/)
|
|
||||||
return values.map((value) => String(value).trim()).filter((value) => ['posts', 'comments', 'favorites', 'credits'].includes(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = extractData(await api.getSchemaPage('users', 'features'))
|
|
||||||
form.dynamicTabs = parseTabs(data.user_center_tabs || 'profile,posts,comments,favorites')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '加载用户功能配置失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await api.saveSchemaPage('users', 'features', { user_center_tabs: ['profile', ...form.dynamicTabs].join(',') })
|
|
||||||
message.success('用户功能配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '保存失败')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UsersSectionPage
|
|
||||||
title="签到奖励"
|
|
||||||
desc="直接管理签到与 Ref 推广奖励,保存后会立即影响前台签到、推广链接和注册页推广码行为。"
|
|
||||||
:links="[
|
|
||||||
{ label: '注册登录', path: '/users/auth' },
|
|
||||||
{ label: '用户积分', path: '/users/credits' },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<template #before-form>
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>当前联动摘要</h3>
|
|
||||||
</div>
|
|
||||||
<n-space>
|
|
||||||
<n-tag round :type="form.credits_enabled ? 'success' : 'warning'">
|
|
||||||
积分系统:{{ form.credits_enabled ? '已启用' : '未启用' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="form.signin_enabled ? 'success' : 'default'">
|
|
||||||
签到能力:{{ form.signin_enabled ? '已启用' : '已关闭' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="form.referral_reward_enabled ? 'success' : 'default'">
|
|
||||||
推广奖励:{{ form.referral_reward_enabled ? '已启用' : '已关闭' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="captchaSummary.register ? 'info' : 'default'">
|
|
||||||
注册验证码:{{ captchaSummary.register ? '启用' : '未启用' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="captchaSummary.signin ? 'info' : 'default'">
|
|
||||||
签到验证码:{{ captchaSummary.signin ? '启用' : '未启用' }}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card v-if="!form.credits_enabled" class="aksrt-section-card">
|
|
||||||
<n-alert type="warning" :show-icon="false">
|
|
||||||
当前积分系统处于关闭状态。即使保存签到和推广奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
|
|
||||||
</n-alert>
|
|
||||||
</n-card>
|
|
||||||
</n-spin>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>每日签到</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-form-item label="启用签到奖励">
|
|
||||||
<n-switch v-model:value="form.signin_enabled" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="基础奖励" v-if="form.signin_enabled">
|
|
||||||
<n-input-number v-model:value="form.signin_base" :min="0" :max="100000" style="width: 180px;" />
|
|
||||||
<template #feedback>
|
|
||||||
<span class="aksrt-hint-text">每次签到基础发放 {{ currencyLabel }} 数量。</span>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="连续签到加成" v-if="form.signin_enabled">
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.signin_streak_text"
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
|
||||||
placeholder="3|2 7|5 30|20"
|
|
||||||
/>
|
|
||||||
<template #feedback>
|
|
||||||
<span class="aksrt-hint-text">每行一个周期,格式 `天数|额外奖励`。例如 `7|5` 表示连续 7 天额外获得 5 {{ currencyLabel }}。</span>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>Ref 推广奖励与注册</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-form-item label="启用推广奖励">
|
|
||||||
<n-switch v-model:value="form.referral_reward_enabled" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<template v-if="form.referral_reward_enabled">
|
|
||||||
<n-form-item label="推广人奖励">
|
|
||||||
<n-input-number v-model:value="form.promoter_amount" :min="0" :max="100000" style="width: 180px;" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="新用户奖励">
|
|
||||||
<n-input-number v-model:value="form.referred_amount" :min="0" :max="100000" style="width: 180px;" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="推广等级积分">
|
|
||||||
<n-input-number v-model:value="form.level_points" :min="0" :max="100000" style="width: 180px;" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="验证邮箱后发放">
|
|
||||||
<n-switch v-model:value="form.require_verify" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="同 IP 日上限">
|
|
||||||
<n-input-number v-model:value="form.ip_daily_cap" :min="0" :max="1000" style="width: 180px;" />
|
|
||||||
</n-form-item>
|
|
||||||
</template>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
|
||||||
</div>
|
|
||||||
</n-form>
|
|
||||||
</UsersSectionPage>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import api from '@/api'
|
|
||||||
import UsersSectionPage from '@/pages/users/UsersSectionPage.vue'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
|
||||||
const creditSnapshot = ref({})
|
|
||||||
|
|
||||||
const captchaSummary = reactive({
|
|
||||||
register: false,
|
|
||||||
signin: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
credits_enabled: true,
|
|
||||||
currency_name: '',
|
|
||||||
signin_enabled: true,
|
|
||||||
signin_base: 1,
|
|
||||||
signin_streak_text: '3|2\n7|5\n30|20',
|
|
||||||
referral_reward_enabled: true,
|
|
||||||
promoter_amount: 5,
|
|
||||||
referred_amount: 2,
|
|
||||||
level_points: 0,
|
|
||||||
require_verify: true,
|
|
||||||
ip_daily_cap: 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
const currencyLabel = computed(() => form.currency_name || '积分')
|
|
||||||
|
|
||||||
function extractData(response) {
|
|
||||||
return response?.data ?? response ?? {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatStreak(streak) {
|
|
||||||
const entries = Object.entries(streak || {})
|
|
||||||
.map(([day, bonus]) => [Number(day), Number(bonus)])
|
|
||||||
.filter(([day, bonus]) => Number.isFinite(day) && day > 0 && Number.isFinite(bonus) && bonus >= 0)
|
|
||||||
.sort((left, right) => left[0] - right[0])
|
|
||||||
|
|
||||||
return entries.map(([day, bonus]) => `${day}|${bonus}`).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStreakText(value) {
|
|
||||||
const rows = String(value || '')
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
const streak = {}
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const parts = row.split('|').map((item) => item.trim())
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const day = Number(parts[0])
|
|
||||||
const bonus = Number(parts[1])
|
|
||||||
if (!Number.isFinite(day) || !Number.isFinite(bonus) || day <= 0 || bonus < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
streak[Math.trunc(day)] = Math.trunc(bonus)
|
|
||||||
})
|
|
||||||
|
|
||||||
return streak
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const [creditRes, captchaRes] = await Promise.all([
|
|
||||||
api.getCreditSettings(),
|
|
||||||
api.getPermissionsLoginSettings(),
|
|
||||||
])
|
|
||||||
|
|
||||||
const creditData = extractData(creditRes)
|
|
||||||
const captchaData = extractData(captchaRes)
|
|
||||||
const dailySignin = creditData.daily_signin || {}
|
|
||||||
const referralReward = creditData.referral_reward || {}
|
|
||||||
const enabledScenarios = Array.isArray(captchaData.enabled_scenarios)
|
|
||||||
? captchaData.enabled_scenarios
|
|
||||||
: []
|
|
||||||
const sceneOn = (key) => enabledScenarios.includes(key)
|
|
||||||
|
|
||||||
creditSnapshot.value = creditData
|
|
||||||
|
|
||||||
form.credits_enabled = creditData.enabled !== false
|
|
||||||
form.currency_name = creditData.currency_name || ''
|
|
||||||
form.signin_enabled = dailySignin.enabled !== false
|
|
||||||
form.signin_base = Number(dailySignin.base || 0)
|
|
||||||
form.signin_streak_text = formatStreak(dailySignin.streak || { 3: 2, 7: 5, 30: 20 })
|
|
||||||
form.referral_reward_enabled = referralReward.enabled !== false
|
|
||||||
form.promoter_amount = Number(referralReward.promoter_amount || 0)
|
|
||||||
form.referred_amount = Number(referralReward.referred_amount || 0)
|
|
||||||
form.level_points = Number(referralReward.level_points || 0)
|
|
||||||
form.require_verify = referralReward.require_verify !== false
|
|
||||||
form.ip_daily_cap = Number(referralReward.ip_daily_cap || 0)
|
|
||||||
|
|
||||||
captchaSummary.register = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('register')
|
|
||||||
captchaSummary.signin = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('signin')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '加载签到奖励配置失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
const nextCredit = {
|
|
||||||
...creditSnapshot.value,
|
|
||||||
daily_signin: {
|
|
||||||
...(creditSnapshot.value.daily_signin || {}),
|
|
||||||
enabled: form.signin_enabled,
|
|
||||||
base: Number(form.signin_base || 0),
|
|
||||||
streak: parseStreakText(form.signin_streak_text),
|
|
||||||
},
|
|
||||||
referral_reward: {
|
|
||||||
...(creditSnapshot.value.referral_reward || {}),
|
|
||||||
enabled: form.referral_reward_enabled,
|
|
||||||
promoter_amount: Number(form.promoter_amount || 0),
|
|
||||||
referred_amount: Number(form.referred_amount || 0),
|
|
||||||
level_points: Number(form.level_points || 0),
|
|
||||||
require_verify: form.require_verify,
|
|
||||||
ip_daily_cap: Number(form.ip_daily_cap || 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.saveCreditSettings(nextCredit)
|
|
||||||
|
|
||||||
creditSnapshot.value = nextCredit
|
|
||||||
message.success('签到奖励配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '保存失败')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export const extensionsPageLoaders = {
|
export const extensionsPageLoaders = {
|
||||||
ExtensionTools: () => import('@/pages/extensions/ExtensionTools.vue'),
|
|
||||||
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export const toolsPageLoaders = {
|
export const toolsPageLoaders = {
|
||||||
ToolsImportExport: () => import('@/pages/tools/ToolsImportExport.vue'),
|
ToolsImportExport: () => import('@/pages/tools/ToolsImportExport.vue'),
|
||||||
ToolsHealth: () => import('@/pages/tools/ToolsHealth.vue'),
|
ToolsHealth: () => import('@/pages/tools/ToolsHealth.vue'),
|
||||||
ToolsEmailRollback: () => import('@/pages/tools/ToolsEmailRollback.vue'),
|
|
||||||
ToolsUpdate: () => import('@/pages/tools/ToolsUpdate.vue'),
|
ToolsUpdate: () => import('@/pages/tools/ToolsUpdate.vue'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export const usersPageLoaders = {
|
export const usersPageLoaders = {
|
||||||
UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
|
|
||||||
UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
|
|
||||||
UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
|
UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
|
||||||
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
|
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
|
||||||
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
|
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const ipSchema = {
|
|||||||
{ label: '文章详细页', value: 'post' },
|
{ label: '文章详细页', value: 'post' },
|
||||||
],
|
],
|
||||||
dependsOn: { key: 'ip_geo_enabled', value: true },
|
dependsOn: { key: 'ip_geo_enabled', value: true },
|
||||||
help: '用户主页/用户中心还需在「用户 → 用户功能」开启「显示城市」;评论区还需在评论设置开启「评论城市」。',
|
help: '用户主页和用户中心由此处直接控制;评论区还需在评论设置开启「评论城市」。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ip_geo_format',
|
key: 'ip_geo_format',
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
export const emailRollbackSchema = {
|
|
||||||
key: 'tools.email-rollback',
|
|
||||||
title: '邮件回滚',
|
|
||||||
icon: 'fa fas fa-undo',
|
|
||||||
description: '批量回滚已发送的通知邮件。',
|
|
||||||
defaults: {},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'rollback',
|
|
||||||
title: '邮件回滚',
|
|
||||||
icon: 'fa fas fa-envelope-open',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'rollback_type',
|
|
||||||
label: '回滚类型',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '按时间范围', value: 'date_range' },
|
|
||||||
{ label: '按邮件类型', value: 'email_type' },
|
|
||||||
{ label: '全部回滚', value: 'all' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ key: 'rollback_date_from', label: '开始日期', type: 'date' },
|
|
||||||
{ key: 'rollback_date_to', label: '结束日期', type: 'date' },
|
|
||||||
{ key: 'rollback_execute', label: '执行回滚', type: 'button', danger: true, help: '此操作不可逆' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
export const frontendModalRolloutSchema = {
|
|
||||||
key: 'tools.frontend-modal-rollout',
|
|
||||||
title: '前台弹窗灰度',
|
|
||||||
icon: 'fa fas fa-bolt',
|
|
||||||
description: '按领域控制前台弹窗新流程的分阶段发布。',
|
|
||||||
defaults: {
|
|
||||||
frontend_modal_rollout_identity: true,
|
|
||||||
frontend_modal_rollout_order: true,
|
|
||||||
frontend_modal_rollout_vip: true,
|
|
||||||
},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'rollout',
|
|
||||||
title: '弹窗新流程开关',
|
|
||||||
icon: 'fa fas fa-toggle-on',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'frontend_modal_rollout_identity',
|
|
||||||
label: '官方认证新流程',
|
|
||||||
type: 'switch',
|
|
||||||
help: '启用统一弹窗运行时与认证快照流程。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'frontend_modal_rollout_order',
|
|
||||||
label: '订单新流程',
|
|
||||||
type: 'switch',
|
|
||||||
help: '启用订单详情快速开壳与状态同步流程。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'frontend_modal_rollout_vip',
|
|
||||||
label: '会员支付新流程',
|
|
||||||
type: 'switch',
|
|
||||||
help: '启用统一会员购买、支付和结果同步流程。',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,10 @@
|
|||||||
|
|
||||||
import { importExportSchema } from './import-export.js'
|
import { importExportSchema } from './import-export.js'
|
||||||
import { healthSchema } from './health.js'
|
import { healthSchema } from './health.js'
|
||||||
import { emailRollbackSchema } from './email-rollback.js'
|
|
||||||
import { backupSchema } from './backup.js'
|
import { backupSchema } from './backup.js'
|
||||||
import { frontendModalRolloutSchema } from './frontend-modal-rollout.js'
|
|
||||||
|
|
||||||
export const toolsSchemas = {
|
export const toolsSchemas = {
|
||||||
'import-export': importExportSchema,
|
'import-export': importExportSchema,
|
||||||
health: healthSchema,
|
health: healthSchema,
|
||||||
'email-rollback': emailRollbackSchema,
|
|
||||||
backup: backupSchema,
|
backup: backupSchema,
|
||||||
'frontend-modal-rollout': frontendModalRolloutSchema,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
|
const themeRoot = typeof window !== 'undefined' && window.aksrtAdminData?.themeUri
|
||||||
|
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
|
||||||
|
: ''
|
||||||
|
|
||||||
export const featuresSchema = {
|
export const featuresSchema = {
|
||||||
key: 'users.features',
|
key: 'users.features',
|
||||||
title: '用户功能',
|
title: '用户功能',
|
||||||
icon: 'fa fas fa-user',
|
icon: 'fa fas fa-user',
|
||||||
description: '配置用户中心和个人主页的功能模块。',
|
description: '配置用户头像、封面和默认签名。',
|
||||||
defaults: {
|
defaults: {
|
||||||
user_center_tabs: 'profile,level,posts,comments,favorites,vip,credits,revenue,auth',
|
avatar_upload_s: true,
|
||||||
user_desc_std: '这家伙很懒…',
|
avatar_external: '',
|
||||||
user_city_s: true,
|
avatar_default_img: `${themeRoot}/assets/img/avatar-default.png`,
|
||||||
user_city_type: 'province',
|
user_cover_upload_s: true,
|
||||||
user_join_day_my_name: '本站',
|
user_cover_img: `${themeRoot}/assets/img/user_t.jpg`,
|
||||||
user_cover_img: '',
|
user_desc_std: '这家伙很懒,什么都没有写...',
|
||||||
user_rewrite_slug: 'user',
|
|
||||||
user_follow_s: true,
|
|
||||||
user_online_s: true,
|
|
||||||
user_last_active_s: true,
|
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
key: 'center',
|
key: 'profile-media',
|
||||||
title: '用户中心',
|
title: '用户资料',
|
||||||
icon: 'fa fas fa-user',
|
icon: 'fa fas fa-user-circle',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,comments,favorites,vip,credits,revenue,auth', help: '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入' },
|
{ key: 'avatar_upload_s', label: '允许用户上传头像', type: 'switch', default: true },
|
||||||
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
|
|
||||||
{ key: 'user_city_s', label: '显示城市(IP 归属地)', type: 'switch', default: true, help: '根据用户登录 IP 在用户主页与用户中心显示归属地;需同时在「扩展 → IP归属地」启用并勾选对应展示位置' },
|
|
||||||
{
|
{
|
||||||
key: 'user_city_type',
|
key: 'avatar_external',
|
||||||
label: '位置精度',
|
label: '外部头像源',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
default: 'province',
|
default: '',
|
||||||
options: [
|
options: [
|
||||||
{ label: '省份', value: 'province' },
|
{ label: '关闭', value: '' },
|
||||||
{ label: '省份+城市', value: 'province_city' },
|
{ label: 'Gravatar', value: 'gravatar' },
|
||||||
{ label: '国家+省份', value: 'country_province' },
|
{ label: 'WeAvatar', value: 'weavatar' },
|
||||||
|
{ label: 'Cravatar', value: 'cravatar' },
|
||||||
|
{ label: '极客族 Gravatar 服务', value: 'geekzu' },
|
||||||
|
{ label: 'loli Gravatar 服务', value: 'loli' },
|
||||||
|
{ label: 'sep.cc Gravatar 服务', value: 'sep' },
|
||||||
|
{ label: 'v2ex Gravatar 服务', value: 'v2ex' },
|
||||||
],
|
],
|
||||||
dependsOn: { key: 'user_city_s', value: true },
|
help: '启用后,用户没有自定义头像时按邮箱获取外部头像。',
|
||||||
},
|
},
|
||||||
{ key: 'user_join_day_my_name', label: '站名', type: 'input', default: '本站', help: '用于「已加入 X 天」显示中的站点名称' },
|
{ key: 'avatar_default_img', label: '用户默认头像', type: 'media', default: `${themeRoot}/assets/img/avatar-default.png`, help: '建议尺寸 100×100。' },
|
||||||
{ key: 'user_cover_img', label: '默认封面', type: 'media', default: '' },
|
{ key: 'user_cover_upload_s', label: '允许用户上传封面', type: 'switch', default: true },
|
||||||
{ key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' },
|
{ key: 'user_cover_img', label: '用户默认封面', type: 'media', default: `${themeRoot}/assets/img/user_t.jpg`, help: '建议尺寸 1000×400;用户未设置封面时显示此图。' },
|
||||||
{ key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true },
|
{ key: 'user_desc_std', label: '用户默认签名', type: 'input', default: '这家伙很懒,什么都没有写...', help: '用户未设置个人简介时显示的默认文案。' },
|
||||||
{ key: 'user_online_s', label: '显示在线状态', type: 'switch', default: true },
|
|
||||||
{ key: 'user_last_active_s', label: '显示最后活跃', type: 'switch', default: true },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { oauthSchema } from './oauth.js'
|
|||||||
import { notificationsSchema } from './notifications.js'
|
import { notificationsSchema } from './notifications.js'
|
||||||
import { creditsSchema } from './credits.js'
|
import { creditsSchema } from './credits.js'
|
||||||
import { featuresSchema } from './features.js'
|
import { featuresSchema } from './features.js'
|
||||||
import { signinSchema } from './signin.js'
|
|
||||||
import { verifySchema } from './verify.js'
|
import { verifySchema } from './verify.js'
|
||||||
import { levelSchema } from './level.js'
|
import { levelSchema } from './level.js'
|
||||||
import { badgeSchema } from './badge.js'
|
import { badgeSchema } from './badge.js'
|
||||||
@@ -21,7 +20,6 @@ export const usersSchemas = {
|
|||||||
notifications: notificationsSchema,
|
notifications: notificationsSchema,
|
||||||
credits: creditsSchema,
|
credits: creditsSchema,
|
||||||
features: featuresSchema,
|
features: featuresSchema,
|
||||||
signin: signinSchema,
|
|
||||||
verify: verifySchema,
|
verify: verifySchema,
|
||||||
level: levelSchema,
|
level: levelSchema,
|
||||||
badge: badgeSchema,
|
badge: badgeSchema,
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
export const signinSchema = {
|
|
||||||
key: 'users.signin',
|
|
||||||
title: '签到奖励',
|
|
||||||
icon: 'fa fas fa-calendar-check',
|
|
||||||
description: '配置签到规则、连续签到奖励和签到页面。',
|
|
||||||
defaults: {
|
|
||||||
signin_enabled: true,
|
|
||||||
signin_reward: 1,
|
|
||||||
signin_consecutive_bonus: true,
|
|
||||||
signin_consecutive_days: 7,
|
|
||||||
signin_bonus_reward: 5,
|
|
||||||
},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'rules',
|
|
||||||
title: '签到规则',
|
|
||||||
icon: 'fa fas fa-calendar',
|
|
||||||
fields: [
|
|
||||||
{ key: 'signin_enabled', label: '启用签到', type: 'switch', default: true },
|
|
||||||
{ key: 'signin_reward', label: '每日签到奖励', type: 'number', default: 1, min: 0, max: 100, dependsOn: { key: 'signin_enabled', value: true } },
|
|
||||||
{ key: 'signin_consecutive_bonus', label: '连续签到加成', type: 'switch', default: true, dependsOn: { key: 'signin_enabled', value: true } },
|
|
||||||
{ key: 'signin_consecutive_days', label: '加成周期 (天)', type: 'number', default: 7, min: 2, max: 30, dependsOn: { key: 'signin_consecutive_bonus', value: true } },
|
|
||||||
{ key: 'signin_bonus_reward', label: '周期奖励', type: 'number', default: 5, min: 1, max: 100, dependsOn: { key: 'signin_consecutive_bonus', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -409,13 +409,6 @@
|
|||||||
return window.AksrtModalCache.invalidate(tags);
|
return window.AksrtModalCache.invalidate(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rolloutEnabled(kind) {
|
|
||||||
var config = window.aksrtData || window.aksrt || {};
|
|
||||||
var rollout = config.frontendModalRollout;
|
|
||||||
if (!rollout || typeof rollout[kind] === "undefined") return true;
|
|
||||||
return rollout[kind] === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.AksrtModalRuntime = {
|
window.AksrtModalRuntime = {
|
||||||
open: open,
|
open: open,
|
||||||
openRemote: openRemote,
|
openRemote: openRemote,
|
||||||
@@ -424,7 +417,6 @@
|
|||||||
registerLoader: registerLoader,
|
registerLoader: registerLoader,
|
||||||
openLoader: openLoader,
|
openLoader: openLoader,
|
||||||
invalidate: invalidate,
|
invalidate: invalidate,
|
||||||
isRolloutEnabled: rolloutEnabled,
|
|
||||||
getActive: function () { return active ? active.shell : null; },
|
getActive: function () { return active ? active.shell : null; },
|
||||||
getState: function () { return active ? active.state : null; },
|
getState: function () { return active ? active.state : null; },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
// ===== VIP 积分兑换 =====
|
// ===== VIP 积分兑换 =====
|
||||||
function initVipPointsExchange() {
|
function initVipPointsExchange() {
|
||||||
var root = document.querySelector('[data-aksrt-vip-points]');
|
var root = document.querySelector('[data-aksrt-vip-points]');
|
||||||
if (!root || !window.AksrtModalRuntime.isRolloutEnabled('vip')) return;
|
if (!root) return;
|
||||||
var tip = root.querySelector('[data-aksrt-vip-points-tip]');
|
var tip = root.querySelector('[data-aksrt-vip-points-tip]');
|
||||||
var busy = false;
|
var busy = false;
|
||||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
// ===== VIP 开通 =====
|
// ===== VIP 开通 =====
|
||||||
function initVipShop() {
|
function initVipShop() {
|
||||||
var root = document.querySelector('[data-aksrt-vip-shop]');
|
var root = document.querySelector('[data-aksrt-vip-shop]');
|
||||||
if (!root || !window.AksrtModalRuntime.isRolloutEnabled('vip')) return;
|
if (!root) return;
|
||||||
var tip = root.querySelector('[data-aksrt-vip-tip]');
|
var tip = root.querySelector('[data-aksrt-vip-tip]');
|
||||||
var busy = false;
|
var busy = false;
|
||||||
var coordinator = window.AksrtVipPurchaseCoordinator;
|
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||||
|
|||||||
@@ -293,7 +293,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initRoot(root) {
|
function initRoot(root) {
|
||||||
if (instances.has(root) || !window.AksrtModalRuntime.isRolloutEnabled("order")) return;
|
if (instances.has(root)) return;
|
||||||
var state = {
|
var state = {
|
||||||
root: root,
|
root: root,
|
||||||
detailOrderNumber: "",
|
detailOrderNumber: "",
|
||||||
|
|||||||
@@ -473,10 +473,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vipPointsPay() {
|
function vipPointsPay() {
|
||||||
if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) {
|
|
||||||
notyf("会员兑换暂不可用", "danger");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.AksrtModalRuntime.openRemote({
|
window.AksrtModalRuntime.openRemote({
|
||||||
url: "/pay/vip/modal",
|
url: "/pay/vip/modal",
|
||||||
query: { vip_level: 1, mode: "points" },
|
query: { vip_level: 1, mode: "points" },
|
||||||
@@ -505,10 +501,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vipPay(trigger) {
|
function vipPay(trigger) {
|
||||||
if (!window.AksrtModalRuntime.isRolloutEnabled("vip")) {
|
|
||||||
notyf("会员购买暂不可用", "danger");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var level = trigger
|
var level = trigger
|
||||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||||
: 1;
|
: 1;
|
||||||
|
|||||||
@@ -318,7 +318,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (!runtime.isRolloutEnabled('identity')) return;
|
|
||||||
stopPolling();
|
stopPolling();
|
||||||
abortCurrent();
|
abortCurrent();
|
||||||
abortRefresh();
|
abortRefresh();
|
||||||
|
|||||||
@@ -351,11 +351,6 @@ if (class_exists('\AKSRT\Core\StatsTracker')) {
|
|||||||
\AKSRT\Core\StatsTracker::init();
|
\AKSRT\Core\StatsTracker::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email rollback window (24h): user list row action + admin-post handler.
|
|
||||||
if (class_exists('\AKSRT\Auth\EmailRollback')) {
|
|
||||||
\AKSRT\Auth\EmailRollback::get_instance()->init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// WordPress 原生"用户"菜单:积分管理
|
// WordPress 原生"用户"菜单:积分管理
|
||||||
if (class_exists('\AKSRT\User\CreditsAdmin')) {
|
if (class_exists('\AKSRT\User\CreditsAdmin')) {
|
||||||
\AKSRT\User\CreditsAdmin::get_instance()->init();
|
\AKSRT\User\CreditsAdmin::get_instance()->init();
|
||||||
|
|||||||
@@ -7,87 +7,9 @@
|
|||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
$system_tools = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_system_tools_settings()
|
|
||||||
: ['close_attachment_page' => true];
|
|
||||||
|
|
||||||
if (!empty($system_tools['close_attachment_page'])) {
|
|
||||||
$attachment_id = get_queried_object_id();
|
$attachment_id = get_queried_object_id();
|
||||||
$parent_id = (int) wp_get_post_parent_id($attachment_id);
|
$parent_id = (int) wp_get_post_parent_id($attachment_id);
|
||||||
$redirect_url = $parent_id > 0 ? get_permalink($parent_id) : home_url('/');
|
$redirect_url = $parent_id > 0 ? get_permalink($parent_id) : home_url('/');
|
||||||
|
|
||||||
wp_safe_redirect($redirect_url, 301);
|
wp_safe_redirect($redirect_url, 301);
|
||||||
exit;
|
exit;
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
|
|
||||||
$attachment_id = get_queried_object_id();
|
|
||||||
$parent_id = (int) wp_get_post_parent_id($attachment_id);
|
|
||||||
$image_url = wp_get_attachment_url($attachment_id);
|
|
||||||
$image_alt = trim((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true));
|
|
||||||
$image_caption = wp_get_attachment_caption($attachment_id);
|
|
||||||
$image_title = get_the_title($attachment_id);
|
|
||||||
$parent_title = $parent_id > 0 ? get_the_title($parent_id) : '';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<main id="main-content" class="container page-content-default aksrt-image-page" tabindex="-1">
|
|
||||||
<div class="content-wrap">
|
|
||||||
<div class="content-layout content-layout--no-sidebar">
|
|
||||||
<div class="content-layout__primary">
|
|
||||||
<?php while (have_posts()) : the_post(); ?>
|
|
||||||
<article class="article main-bg theme-box box-body radius8 main-shadow aksrt-image-attachment">
|
|
||||||
<div class="article-header theme-box clearfix relative">
|
|
||||||
<h1 class="article-title">
|
|
||||||
<a href="<?php the_permalink(); ?>"><?php echo esc_html($image_title); ?></a>
|
|
||||||
</h1>
|
|
||||||
<?php if ($parent_id > 0) : ?>
|
|
||||||
<div class="em09 muted-3-color aksrt-image-attachment__parent">
|
|
||||||
<?php esc_html_e('所属内容:', 'aksrt'); ?>
|
|
||||||
<a href="<?php echo esc_url(get_permalink($parent_id)); ?>"><?php echo esc_html($parent_title); ?></a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wp-posts-content" data-nav="posts">
|
|
||||||
<?php if ($image_url) : ?>
|
|
||||||
<figure class="aksrt-image-attachment__figure">
|
|
||||||
<a href="<?php echo esc_url($image_url); ?>" class="item-thumbnail fit-cover" target="_blank" rel="noopener noreferrer">
|
|
||||||
<?php echo wp_get_attachment_image($attachment_id, 'full', false, ['class' => 'fit-cover', 'alt' => $image_alt !== '' ? $image_alt : $image_title]); ?>
|
|
||||||
</a>
|
|
||||||
<?php if ($image_caption) : ?>
|
|
||||||
<figcaption class="muted-2-color em09"><?php echo esc_html($image_caption); ?></figcaption>
|
|
||||||
<?php endif; ?>
|
|
||||||
</figure>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (get_the_content() !== '') : ?>
|
|
||||||
<div class="aksrt-image-attachment__content">
|
|
||||||
<?php the_content(); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center post-actions aksrt-image-attachment__actions">
|
|
||||||
<?php if ($image_url) : ?>
|
|
||||||
<a href="<?php echo esc_url($image_url); ?>" class="action action-share" target="_blank" rel="noopener noreferrer">
|
|
||||||
<i class="fa fas fa-image" aria-hidden="true"></i>
|
|
||||||
<span><?php esc_html_e('查看原图', 'aksrt'); ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($parent_id > 0) : ?>
|
|
||||||
<a href="<?php echo esc_url(get_permalink($parent_id)); ?>" class="action action-favorite">
|
|
||||||
<i class="fa fas fa-arrow-left" aria-hidden="true"></i>
|
|
||||||
<span><?php esc_html_e('返回文章', 'aksrt'); ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endwhile; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
get_footer();
|
|
||||||
|
|||||||
@@ -37,18 +37,98 @@ if (!function_exists('aksrt_resolve_avatar_user_id')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_resolve_avatar_email')) {
|
||||||
|
/**
|
||||||
|
* Resolve an email address from an avatar subject.
|
||||||
|
*
|
||||||
|
* @param mixed $subject User ID, WP_User, comment, post, or email.
|
||||||
|
*/
|
||||||
|
function aksrt_resolve_avatar_email($subject): string {
|
||||||
|
if ($subject instanceof WP_User) {
|
||||||
|
return sanitize_email((string) $subject->user_email);
|
||||||
|
}
|
||||||
|
if ($subject instanceof WP_Comment && is_email((string) $subject->comment_author_email)) {
|
||||||
|
return sanitize_email((string) $subject->comment_author_email);
|
||||||
|
}
|
||||||
|
if (is_object($subject) && isset($subject->comment_author_email) && is_email((string) $subject->comment_author_email)) {
|
||||||
|
return sanitize_email((string) $subject->comment_author_email);
|
||||||
|
}
|
||||||
|
if (is_string($subject) && is_email($subject)) {
|
||||||
|
return sanitize_email($subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = aksrt_resolve_avatar_user_id($subject);
|
||||||
|
$user = $user_id > 0 ? get_userdata($user_id) : false;
|
||||||
|
return $user instanceof WP_User ? sanitize_email((string) $user->user_email) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_get_user_feature_settings')) {
|
||||||
|
/**
|
||||||
|
* Return the canonical user media and signature settings.
|
||||||
|
*
|
||||||
|
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
|
||||||
|
*/
|
||||||
|
function aksrt_get_user_feature_settings(): array {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
return \AKSRT\Settings\Settings::get_user_feature_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'avatar_upload_s' => true,
|
||||||
|
'avatar_external' => '',
|
||||||
|
'avatar_default_img' => get_theme_file_uri('assets/img/avatar-default.png'),
|
||||||
|
'user_cover_upload_s' => true,
|
||||||
|
'user_cover_img' => get_theme_file_uri('assets/img/user_t.jpg'),
|
||||||
|
'user_desc_std' => '这家伙很懒,什么都没有写...',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!function_exists('aksrt_get_default_avatar_url')) {
|
if (!function_exists('aksrt_get_default_avatar_url')) {
|
||||||
/**
|
/**
|
||||||
* Return the bundled avatar used when a user has no custom image.
|
* Return the bundled avatar used when a user has no custom image.
|
||||||
*/
|
*/
|
||||||
function aksrt_get_default_avatar_url(): string {
|
function aksrt_get_default_avatar_url(): string {
|
||||||
|
$settings = aksrt_get_user_feature_settings();
|
||||||
return (string) apply_filters(
|
return (string) apply_filters(
|
||||||
'aksrt_default_avatar_url',
|
'aksrt_default_avatar_url',
|
||||||
get_theme_file_uri('assets/img/user/default-avatar.svg')
|
(string) $settings['avatar_default_img']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_get_external_avatar_url')) {
|
||||||
|
/**
|
||||||
|
* Return the configured external avatar for a subject.
|
||||||
|
*
|
||||||
|
* @param mixed $subject User subject.
|
||||||
|
*/
|
||||||
|
function aksrt_get_external_avatar_url($subject, int $size = 96): string {
|
||||||
|
$settings = aksrt_get_user_feature_settings();
|
||||||
|
$hosts = [
|
||||||
|
'gravatar' => 'https://www.gravatar.com/avatar/',
|
||||||
|
'weavatar' => 'https://weavatar.com/avatar/',
|
||||||
|
'cravatar' => 'https://cn.cravatar.com/avatar/',
|
||||||
|
'geekzu' => 'https://sdn.geekzu.org/avatar/',
|
||||||
|
'loli' => 'https://gravatar.loli.net/avatar/',
|
||||||
|
'sep' => 'https://cdn.sep.cc/avatar/',
|
||||||
|
'v2ex' => 'https://cdn.v2ex.com/gravatar/',
|
||||||
|
];
|
||||||
|
$host = $hosts[(string) $settings['avatar_external']] ?? '';
|
||||||
|
$email = aksrt_resolve_avatar_email($subject);
|
||||||
|
if ($host === '' || $email === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = max(1, min(2048, $size));
|
||||||
|
return add_query_arg([
|
||||||
|
's' => $size,
|
||||||
|
'd' => aksrt_get_default_avatar_url(),
|
||||||
|
], $host . hash('sha256', strtolower(trim($email))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!function_exists('aksrt_get_custom_avatar_url')) {
|
if (!function_exists('aksrt_get_custom_avatar_url')) {
|
||||||
/**
|
/**
|
||||||
* Return the locally uploaded avatar URL.
|
* Return the locally uploaded avatar URL.
|
||||||
@@ -91,6 +171,10 @@ if (!function_exists('aksrt_get_avatar_url')) {
|
|||||||
if ($custom !== '') {
|
if ($custom !== '') {
|
||||||
return $custom;
|
return $custom;
|
||||||
}
|
}
|
||||||
|
$external = aksrt_get_external_avatar_url($subject, $size);
|
||||||
|
if ($external !== '') {
|
||||||
|
return $external;
|
||||||
|
}
|
||||||
return aksrt_get_default_avatar_url();
|
return aksrt_get_default_avatar_url();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +217,7 @@ if (!function_exists('aksrt_get_avatar_html')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add_filter('get_avatar_data', static function (array $args, $subject): array {
|
add_filter('get_avatar_data', static function (array $args, $subject): array {
|
||||||
$user_id = aksrt_resolve_avatar_user_id($subject);
|
$args['url'] = aksrt_get_avatar_url($subject, ['size' => (int) ($args['size'] ?? 96)]);
|
||||||
$custom = aksrt_get_custom_avatar_url($user_id, (int) ($args['size'] ?? 96));
|
|
||||||
$args['url'] = $custom !== '' ? $custom : aksrt_get_default_avatar_url();
|
|
||||||
$args['found_avatar'] = true;
|
$args['found_avatar'] = true;
|
||||||
return $args;
|
return $args;
|
||||||
}, 20, 2);
|
}, 20, 2);
|
||||||
@@ -151,7 +233,37 @@ if (!function_exists('aksrt_is_avatar_upload_enabled')) {
|
|||||||
* Whether local avatar uploads are enabled by attachment policy.
|
* Whether local avatar uploads are enabled by attachment policy.
|
||||||
*/
|
*/
|
||||||
function aksrt_is_avatar_upload_enabled(): bool {
|
function aksrt_is_avatar_upload_enabled(): bool {
|
||||||
return function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', get_current_user_id());
|
$settings = aksrt_get_user_feature_settings();
|
||||||
|
return !empty($settings['avatar_upload_s']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_is_user_cover_upload_enabled')) {
|
||||||
|
function aksrt_is_user_cover_upload_enabled(): bool {
|
||||||
|
$settings = aksrt_get_user_feature_settings();
|
||||||
|
return !empty($settings['user_cover_upload_s']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_get_user_cover_url')) {
|
||||||
|
function aksrt_get_user_cover_url(int $user_id): string {
|
||||||
|
$custom = $user_id > 0 ? esc_url_raw((string) get_user_meta($user_id, 'aksrt_background_image', true)) : '';
|
||||||
|
if ($custom !== '') {
|
||||||
|
return $custom;
|
||||||
|
}
|
||||||
|
$settings = aksrt_get_user_feature_settings();
|
||||||
|
return (string) $settings['user_cover_img'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('aksrt_get_user_description')) {
|
||||||
|
function aksrt_get_user_description(int $user_id): string {
|
||||||
|
$description = $user_id > 0 ? trim((string) get_user_meta($user_id, 'description', true)) : '';
|
||||||
|
if ($description !== '') {
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
$settings = aksrt_get_user_feature_settings();
|
||||||
|
return (string) $settings['user_desc_std'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,10 +261,7 @@ if (!function_exists('aksrt_author_minicard')) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$desc = trim((string) get_user_meta($user_id, 'description', true));
|
$desc = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : trim((string) get_user_meta($user_id, 'description', true));
|
||||||
if ($desc === '') {
|
|
||||||
$desc = __('这家伙很懒,什么都没有写…', 'aksrt');
|
|
||||||
}
|
|
||||||
|
|
||||||
$blur = '';
|
$blur = '';
|
||||||
if ($blur_bg) {
|
if ($blur_bg) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
* Gates:
|
* Gates:
|
||||||
* master: ip_geo_enabled
|
* master: ip_geo_enabled
|
||||||
* surface: ip_geo_display ∈ {comment, user_home, user_center, post}
|
* surface: ip_geo_display ∈ {comment, user_home, user_center, post}
|
||||||
* users: user_city_s (+ user_city_type)
|
|
||||||
* comments: comment_city_s (+ comment_city_type) via comments/geo.php
|
* comments: comment_city_s (+ comment_city_type) via comments/geo.php
|
||||||
*
|
*
|
||||||
* Meta:
|
* Meta:
|
||||||
@@ -74,9 +73,6 @@ if (!function_exists('aksrt_ip_geo_display_enabled')) :
|
|||||||
if (!in_array($location, aksrt_ip_geo_display_list(), true)) {
|
if (!in_array($location, aksrt_ip_geo_display_list(), true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (in_array($location, ['user_home', 'user_center'], true) && !aksrt_user_city_enabled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
@@ -90,37 +86,6 @@ if (!function_exists('aksrt_ip_geo_format_key')) :
|
|||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_user_city_enabled')) :
|
|
||||||
/**
|
|
||||||
* Users feature: show city on user home / user center.
|
|
||||||
* Default true when key absent (legacy parity for new installs).
|
|
||||||
*/
|
|
||||||
function aksrt_user_city_enabled(): bool {
|
|
||||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$all = \AKSRT\Settings\Settings::get_all();
|
|
||||||
if (!array_key_exists('user_city_s', $all)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !empty($all['user_city_s']);
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (!function_exists('aksrt_user_city_type')) :
|
|
||||||
function aksrt_user_city_type(): string {
|
|
||||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
|
||||||
return aksrt_ip_geo_format_key();
|
|
||||||
}
|
|
||||||
$all = \AKSRT\Settings\Settings::get_all();
|
|
||||||
$type = sanitize_key((string) ($all['user_city_type'] ?? ''));
|
|
||||||
if (in_array($type, ['province_city', 'province', 'country_province'], true)) {
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
return aksrt_ip_geo_format_key();
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (!function_exists('aksrt_normalize_geo_addr')) :
|
if (!function_exists('aksrt_normalize_geo_addr')) :
|
||||||
/**
|
/**
|
||||||
* @param mixed $raw Meta value.
|
* @param mixed $raw Meta value.
|
||||||
@@ -229,8 +194,7 @@ if (!function_exists('aksrt_store_user_addr')) :
|
|||||||
if ($user_id <= 0 || !aksrt_ip_geo_master_enabled()) {
|
if ($user_id <= 0 || !aksrt_ip_geo_master_enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$need = aksrt_user_city_enabled()
|
$need = in_array('post', aksrt_ip_geo_display_list(), true)
|
||||||
|| in_array('post', aksrt_ip_geo_display_list(), true)
|
|
||||||
|| in_array('user_home', aksrt_ip_geo_display_list(), true)
|
|| in_array('user_home', aksrt_ip_geo_display_list(), true)
|
||||||
|| in_array('user_center', aksrt_ip_geo_display_list(), true);
|
|| in_array('user_center', aksrt_ip_geo_display_list(), true);
|
||||||
if (!$need) {
|
if (!$need) {
|
||||||
@@ -286,7 +250,7 @@ if (!function_exists('aksrt_get_user_city_badge_html')) :
|
|||||||
if ($user_id <= 0 || !aksrt_ip_geo_display_enabled($location)) {
|
if ($user_id <= 0 || !aksrt_ip_geo_display_enabled($location)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return aksrt_get_geo_badge_html(aksrt_get_user_addr($user_id), aksrt_user_city_type(), $class);
|
return aksrt_get_geo_badge_html(aksrt_get_user_addr($user_id), aksrt_ip_geo_format_key(), $class);
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -304,8 +268,7 @@ if (!function_exists('aksrt_get_post_author_city_badge_html')) :
|
|||||||
if ($author_id <= 0) {
|
if ($author_id <= 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$type = aksrt_user_city_enabled() ? aksrt_user_city_type() : aksrt_ip_geo_format_key();
|
return aksrt_get_geo_badge_html(aksrt_get_user_addr($author_id), aksrt_ip_geo_format_key(), $class);
|
||||||
return aksrt_get_geo_badge_html(aksrt_get_user_addr($author_id), $type, $class);
|
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
|||||||
}
|
}
|
||||||
$author_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($author_id) : get_author_posts_url($author_id);
|
$author_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($author_id) : get_author_posts_url($author_id);
|
||||||
$author_name = get_the_author();
|
$author_name = get_the_author();
|
||||||
$bio = trim((string) get_the_author_meta('description'));
|
$bio = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($author_id) : trim((string) get_the_author_meta('description'));
|
||||||
$name_html = function_exists('aksrt_get_user_name')
|
$name_html = function_exists('aksrt_get_user_name')
|
||||||
? aksrt_get_user_name([
|
? aksrt_get_user_name([
|
||||||
'id' => $author_id,
|
'id' => $author_id,
|
||||||
|
|||||||
@@ -137,11 +137,11 @@ if (!function_exists('aksrt_get_user_center_tab_url')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!function_exists('aksrt_normalize_user_center_tabs')) {
|
if (!function_exists('aksrt_normalize_user_center_panels')) {
|
||||||
/**
|
/**
|
||||||
* Normalize the dashboard tab contract.
|
* Normalize the dashboard tab contract.
|
||||||
*/
|
*/
|
||||||
function aksrt_normalize_user_center_tabs(array $tabs): array {
|
function aksrt_normalize_user_center_panels(array $tabs): array {
|
||||||
$normalized = [];
|
$normalized = [];
|
||||||
foreach ($tabs as $key => $tab) {
|
foreach ($tabs as $key => $tab) {
|
||||||
$key = sanitize_key((string) $key);
|
$key = sanitize_key((string) $key);
|
||||||
@@ -296,7 +296,7 @@ if (!function_exists('aksrt_user_center_resolve_tabs')) {
|
|||||||
'favorites' => ['label' => __('我的收藏', 'aksrt'), 'icon' => 'star'],
|
'favorites' => ['label' => __('我的收藏', 'aksrt'), 'icon' => 'star'],
|
||||||
'settings' => ['label' => __('账号设置', 'aksrt'), 'icon' => 'cog'],
|
'settings' => ['label' => __('账号设置', 'aksrt'), 'icon' => 'cog'],
|
||||||
];
|
];
|
||||||
$tabs = aksrt_normalize_user_center_tabs($tabs);
|
$tabs = aksrt_normalize_user_center_panels($tabs);
|
||||||
$keys = array_keys($tabs);
|
$keys = array_keys($tabs);
|
||||||
$default = in_array('vip', $keys, true)
|
$default = in_array('vip', $keys, true)
|
||||||
? 'vip'
|
? 'vip'
|
||||||
@@ -420,7 +420,7 @@ if (!function_exists('aksrt_user_center_cover_upload_config')) {
|
|||||||
'imageId' => 'aksrt-cover-img',
|
'imageId' => 'aksrt-cover-img',
|
||||||
'fieldName' => 'background',
|
'fieldName' => 'background',
|
||||||
'maxSize' => $max_size,
|
'maxSize' => $max_size,
|
||||||
'fallbackImage' => AKSRT_URI . '/assets/img/person_background.png',
|
'fallbackImage' => function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url(get_current_user_id()) : get_theme_file_uri('assets/img/user_t.jpg'),
|
||||||
'idleText' => __('修改背景图', 'aksrt'),
|
'idleText' => __('修改背景图', 'aksrt'),
|
||||||
'loadingText' => __('上传中…', 'aksrt'),
|
'loadingText' => __('上传中…', 'aksrt'),
|
||||||
'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
|
'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
|
||||||
@@ -459,9 +459,9 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
|||||||
$comment_count = (int) ($dashboard['comment_count'] ?? get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']));
|
$comment_count = (int) ($dashboard['comment_count'] ?? get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']));
|
||||||
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
|
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
|
||||||
$favorite_count = (int) ($dashboard['favorite_count'] ?? count($favorite_post_ids));
|
$favorite_count = (int) ($dashboard['favorite_count'] ?? count($favorite_post_ids));
|
||||||
$user_background_url = (string) ($dashboard['background_url'] ?? get_user_meta($user_id, 'aksrt_background_image', true));
|
$user_background_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
|
||||||
$allow_avatar_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', $user_id);
|
$allow_avatar_upload = function_exists('aksrt_is_avatar_upload_enabled') && aksrt_is_avatar_upload_enabled();
|
||||||
$allow_bg_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_profile_background', $user_id);
|
$allow_bg_upload = function_exists('aksrt_is_user_cover_upload_enabled') && aksrt_is_user_cover_upload_enabled();
|
||||||
$messages_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::message_enabled();
|
$messages_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::message_enabled();
|
||||||
$follower_count = class_exists('\AKSRT\User\Follow')
|
$follower_count = class_exists('\AKSRT\User\Follow')
|
||||||
? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id)
|
? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
|
|||||||
? aksrt_get_visible_user_profile_details($user_id, $viewer_id)
|
? aksrt_get_visible_user_profile_details($user_id, $viewer_id)
|
||||||
: ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'github' => ''];
|
: ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'github' => ''];
|
||||||
|
|
||||||
$background_url = get_user_meta($user_id, 'aksrt_background_image', true) ?: '';
|
$background_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
|
||||||
|
|
||||||
$credits_enabled = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
|
$credits_enabled = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
|
||||||
$credit_balance = $credits_enabled && $is_own_profile && function_exists('aksrt_user_credits')
|
$credit_balance = $credits_enabled && $is_own_profile && function_exists('aksrt_user_credits')
|
||||||
|
|||||||
@@ -67,10 +67,7 @@ if (!function_exists('aksrt_get_header_nav_context')) :
|
|||||||
|
|
||||||
$current_user = wp_get_current_user();
|
$current_user = wp_get_current_user();
|
||||||
$user_id = (int) $current_user->ID;
|
$user_id = (int) $current_user->ID;
|
||||||
$user_desc = $user_id > 0 ? (string) get_user_meta($user_id, 'description', true) : '';
|
$user_desc = $user_id > 0 && function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : '';
|
||||||
if ($user_desc === '' && $user_id > 0) {
|
|
||||||
$user_desc = (string) $current_user->user_email;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ctx = [
|
$ctx = [
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
|||||||
?>
|
?>
|
||||||
<div class="uc-header">
|
<div class="uc-header">
|
||||||
<div class="uc-header__cover">
|
<div class="uc-header__cover">
|
||||||
<img id="aksrt-cover-img" class="uc-header__cover-img" src="<?php echo esc_url($user_background_url ? $user_background_url : get_theme_file_uri('assets/img/person_background.png')); ?>" alt="">
|
<img id="aksrt-cover-img" class="uc-header__cover-img" src="<?php echo esc_url($user_background_url); ?>" alt="">
|
||||||
<div class="uc-header__cover-mask"></div>
|
<div class="uc-header__cover-mask"></div>
|
||||||
<div class="aksrt-cover-menu" data-aksrt-menu>
|
<div class="aksrt-cover-menu" data-aksrt-menu>
|
||||||
<button type="button" class="aksrt-cover-menu__trigger" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e('个人主页操作', 'aksrt'); ?>">
|
<button type="button" class="aksrt-cover-menu__trigger" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e('个人主页操作', 'aksrt'); ?>">
|
||||||
@@ -58,7 +58,7 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
|||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
// IP 归属地 (legacy user_page_header_desc + user_city_s).
|
// IP 归属地由扩展设置统一控制。
|
||||||
$uc_city = function_exists('aksrt_get_user_city_badge_html')
|
$uc_city = function_exists('aksrt_get_user_city_badge_html')
|
||||||
? aksrt_get_user_city_badge_html((int) $user_id, 'user_center', 'badg')
|
? aksrt_get_user_city_badge_html((int) $user_id, 'user_center', 'badg')
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -302,10 +302,6 @@ add_action('wp_ajax_aksrt_change_email', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the account-change audit window; registration verification is pre-create only.
|
|
||||||
update_user_meta( $user_id, 'aksrt_email_previous', $old_email );
|
|
||||||
update_user_meta( $user_id, 'aksrt_email_changed_at', time() );
|
|
||||||
|
|
||||||
$verify_url = '';
|
$verify_url = '';
|
||||||
$verify_message = '';
|
$verify_message = '';
|
||||||
update_user_meta( $user_id, 'aksrt_email_verified', 1 );
|
update_user_meta( $user_id, 'aksrt_email_verified', 1 );
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ class SchemaTools {
|
|||||||
'import-export' => SchemaToolsImportExport::build(),
|
'import-export' => SchemaToolsImportExport::build(),
|
||||||
'health' => SchemaToolsHealth::build(),
|
'health' => SchemaToolsHealth::build(),
|
||||||
'backup' => SchemaToolsBackup::build(),
|
'backup' => SchemaToolsBackup::build(),
|
||||||
'email-rollback' => SchemaToolsEmailRollback::build(),
|
|
||||||
'frontend-modal-rollout' => SchemaToolsFrontendModalRollout::build(),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package AKSRT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace AKSRT\Admin\Schema;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class SchemaToolsEmailRollback {
|
|
||||||
|
|
||||||
public static function build(): array {
|
|
||||||
return [
|
|
||||||
'key' => 'email-rollback',
|
|
||||||
'title' => '邮件回滚',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'option_key' => 'aksrt_settings',
|
|
||||||
'groups' => [
|
|
||||||
['key' => 'rollback', 'title' => '邮件回滚', 'fields' => [
|
|
||||||
['key' => 'rollback_type', 'label' => '回滚类型', 'type' => 'select', 'options' => [
|
|
||||||
['label' => '按时间范围', 'value' => 'date_range'],
|
|
||||||
['label' => '按邮件类型', 'value' => 'email_type'],
|
|
||||||
['label' => '全部回滚', 'value' => 'all'],
|
|
||||||
]],
|
|
||||||
['key' => 'rollback_date_from', 'label' => '开始日期', 'type' => 'date'],
|
|
||||||
['key' => 'rollback_date_to', 'label' => '结束日期', 'type' => 'date'],
|
|
||||||
['key' => 'rollback_execute', 'label' => '执行回滚', 'type' => 'button', 'danger' => true, 'help' => '此操作不可逆'],
|
|
||||||
]],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package AKSRT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace AKSRT\Admin\Schema;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class SchemaToolsFrontendModalRollout {
|
|
||||||
|
|
||||||
public static function build(): array {
|
|
||||||
return [
|
|
||||||
'key' => 'frontend-modal-rollout',
|
|
||||||
'title' => '前台弹窗灰度',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'option_key' => 'aksrt_settings',
|
|
||||||
'groups' => [
|
|
||||||
[
|
|
||||||
'key' => 'rollout',
|
|
||||||
'title' => '弹窗新流程开关',
|
|
||||||
'fields' => [
|
|
||||||
[
|
|
||||||
'key' => 'frontend_modal_rollout_identity',
|
|
||||||
'label' => '官方认证新流程',
|
|
||||||
'type' => 'switch',
|
|
||||||
'default' => true,
|
|
||||||
'help' => '启用统一弹窗运行时与认证快照流程。',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'key' => 'frontend_modal_rollout_order',
|
|
||||||
'label' => '订单新流程',
|
|
||||||
'type' => 'switch',
|
|
||||||
'default' => true,
|
|
||||||
'help' => '启用订单详情快速开壳与状态同步流程。',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'key' => 'frontend_modal_rollout_vip',
|
|
||||||
'label' => '会员支付新流程',
|
|
||||||
'type' => 'switch',
|
|
||||||
'default' => true,
|
|
||||||
'help' => '启用统一会员购买、支付和结果同步流程。',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -475,26 +475,6 @@ class SchemaUsers {
|
|||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'signin' => [
|
|
||||||
'key' => 'signin',
|
|
||||||
'title' => '签到奖励',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'option_key' => 'aksrt_settings',
|
|
||||||
'groups' => [
|
|
||||||
['key' => 'rules', 'title' => '签到规则', 'fields' => [
|
|
||||||
['key' => 'signin_enabled', 'label' => '启用签到', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'signin_reward', 'label' => '每日签到奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
|
||||||
['key' => 'signin_consecutive_bonus', 'label' => '连续签到加成', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'signin_consecutive_days', 'label' => '加成周期', 'type' => 'number', 'default' => 7, 'min' => 2, 'max' => 30],
|
|
||||||
['key' => 'signin_bonus_reward', 'label' => '周期奖励', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
|
|
||||||
]],
|
|
||||||
['key' => 'display', 'title' => '签到展示', 'fields' => [
|
|
||||||
['key' => 'signin_header_user_show', 'label' => '导航栏显示签到', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'signin_modal_auto_s', 'label' => '自动弹出签到', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'signin_modal_auto_expires', 'label' => '弹出间隔(天)', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 30],
|
|
||||||
]],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'credits' => [
|
'credits' => [
|
||||||
'key' => 'credits',
|
'key' => 'credits',
|
||||||
'title' => '积分设置',
|
'title' => '积分设置',
|
||||||
@@ -551,72 +531,23 @@ class SchemaUsers {
|
|||||||
'version' => '1.0.0',
|
'version' => '1.0.0',
|
||||||
'option_key' => 'aksrt_settings',
|
'option_key' => 'aksrt_settings',
|
||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
['key' => 'profile-media', 'title' => '用户资料', 'fields' => [
|
||||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入'],
|
['key' => 'avatar_upload_s', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
['key' => 'avatar_external', 'label' => '外部头像源', 'type' => 'radio', 'default' => '', 'options' => [
|
||||||
['key' => 'user_city_s', 'label' => '显示城市(IP 归属地)', 'type' => 'switch', 'default' => true],
|
['label' => '关闭', 'value' => ''],
|
||||||
['key' => 'user_city_type', 'label' => '位置精度', 'type' => 'radio', 'default' => 'province', 'options' => [
|
['label' => 'Gravatar', 'value' => 'gravatar'],
|
||||||
['label' => '省份', 'value' => 'province'],
|
['label' => 'WeAvatar', 'value' => 'weavatar'],
|
||||||
['label' => '省份+城市', 'value' => 'province_city'],
|
['label' => 'Cravatar', 'value' => 'cravatar'],
|
||||||
['label' => '国家+省份', 'value' => 'country_province'],
|
['label' => '极客族 Gravatar 服务', 'value' => 'geekzu'],
|
||||||
|
['label' => 'loli Gravatar 服务', 'value' => 'loli'],
|
||||||
|
['label' => 'sep.cc Gravatar 服务', 'value' => 'sep'],
|
||||||
|
['label' => 'v2ex Gravatar 服务', 'value' => 'v2ex'],
|
||||||
|
], 'help' => '启用后,用户没有自定义头像时按邮箱获取外部头像。'],
|
||||||
|
['key' => 'avatar_default_img', 'label' => '用户默认头像', 'type' => 'media', 'default' => get_theme_file_uri('assets/img/avatar-default.png'), 'help' => '建议尺寸 100×100。'],
|
||||||
|
['key' => 'user_cover_upload_s', 'label' => '允许用户上传封面', 'type' => 'switch', 'default' => true],
|
||||||
|
['key' => 'user_cover_img', 'label' => '用户默认封面', 'type' => 'media', 'default' => get_theme_file_uri('assets/img/user_t.jpg'), 'help' => '建议尺寸 1000×400;用户未设置封面时显示此图。'],
|
||||||
|
['key' => 'user_desc_std', 'label' => '用户默认签名', 'type' => 'input', 'default' => '这家伙很懒,什么都没有写...', 'help' => '用户未设置个人简介时显示的默认文案。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
|
||||||
['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
|
|
||||||
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
|
|
||||||
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_online_s', 'label' => '显示在线状态', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_last_active_s', 'label' => '显示最后活跃', 'type' => 'switch', 'default' => true],
|
|
||||||
]],
|
|
||||||
['key' => 'profile', 'title' => '资料页字段', 'fields' => [
|
|
||||||
['key' => 'user_profile_nickname', 'label' => '昵称', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_email', 'label' => '邮箱', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_url', 'label' => '个人主页', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_desc', 'label' => '个人简介', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_avatar', 'label' => '头像', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_profile_qq', 'label' => 'QQ', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_wechat', 'label' => '微信', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_weibo', 'label' => '微博', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_github', 'label' => 'GitHub', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_bilibili', 'label' => 'B站', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_twitter', 'label' => 'Twitter', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_linkedin', 'label' => 'LinkedIn', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_facebook', 'label' => 'Facebook', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_telegram', 'label' => 'Telegram', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_douyin', 'label' => '抖音', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_profile_zhihu', 'label' => '知乎', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
|
||||||
['key' => 'card', 'title' => '用户卡片', 'fields' => [
|
|
||||||
['key' => 'user_card_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_card_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_card_desc', 'label' => '显示简介', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_card_count', 'label' => '显示统计', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_card_follow', 'label' => '显示关注按钮', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'user_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'right', 'options' => [
|
|
||||||
['label' => '右侧', 'value' => 'right'],
|
|
||||||
['label' => '左侧', 'value' => 'left'],
|
|
||||||
['label' => '顶部', 'value' => 'top'],
|
|
||||||
]],
|
|
||||||
['key' => 'user_card_style', 'label' => '卡片样式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
|
||||||
['label' => '默认', 'value' => 'default'],
|
|
||||||
['label' => '简洁', 'value' => 'simple'],
|
|
||||||
['label' => '详细', 'value' => 'detail'],
|
|
||||||
]],
|
|
||||||
['key' => 'user_card_bg', 'label' => '卡片背景', 'type' => 'media', 'default' => ''],
|
|
||||||
['key' => 'user_card_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 80, 'min' => 40, 'max' => 200],
|
|
||||||
]],
|
|
||||||
['key' => 'privacy', 'title' => '隐私设置', 'fields' => [
|
|
||||||
['key' => 'user_privacy_posts', 'label' => '隐藏文章', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_ip', 'label' => '隐藏IP归属地', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'user_privacy_location', 'label' => '隐藏位置', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
|
||||||
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'verify' => [
|
'verify' => [
|
||||||
|
|||||||
@@ -17,34 +17,6 @@ class ToolsController extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
// Email rollback: list users with recent email changes
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'rest_email_rollback_list'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Email rollback: execute rollback
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users/(?P<id>\d+)/rollback', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'rest_email_rollback_execute'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Tools: system health (compact format for ExtensionTools.vue)
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/tools/health', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'get_tools_health'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Tools: maintenance actions
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/tools/maintenance', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'run_maintenance'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Tools: update center
|
// Tools: update center
|
||||||
register_rest_route(self::API_NAMESPACE, '/tools/update-info', [
|
register_rest_route(self::API_NAMESPACE, '/tools/update-info', [
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
@@ -65,85 +37,6 @@ class ToolsController extends ControllerBase {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /tools/health — compact health data for the tools page.
|
|
||||||
*/
|
|
||||||
public function get_tools_health(): \WP_REST_Response {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$upload_dir = wp_upload_dir();
|
|
||||||
$upload_ok = !empty($upload_dir['error']) === false && is_dir($upload_dir['basedir']);
|
|
||||||
|
|
||||||
$db_ok = true;
|
|
||||||
$db_status = '正常';
|
|
||||||
$expected_tables = [
|
|
||||||
$wpdb->prefix . 'aksrt_credit_log',
|
|
||||||
$wpdb->prefix . 'aksrt_signin_log',
|
|
||||||
$wpdb->prefix . 'aksrt_dl_token',
|
|
||||||
$wpdb->prefix . 'aksrt_notification',
|
|
||||||
];
|
|
||||||
$missing = [];
|
|
||||||
foreach ($expected_tables as $table) {
|
|
||||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
|
|
||||||
$missing[] = str_replace($wpdb->prefix, '', $table);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!empty($missing)) {
|
|
||||||
$db_ok = false;
|
|
||||||
$db_status = '缺少: ' . implode(', ', $missing);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'data' => [
|
|
||||||
'php_version' => PHP_VERSION,
|
|
||||||
'wp_version' => get_bloginfo('version'),
|
|
||||||
'db_status' => $db_status,
|
|
||||||
'db_ok' => $db_ok,
|
|
||||||
'upload_dir' => $upload_dir['basedir'],
|
|
||||||
'upload_ok' => $upload_ok,
|
|
||||||
'theme_version' => wp_get_theme()->get('Version') ?? 'unknown',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /tools/maintenance — run a maintenance action.
|
|
||||||
*/
|
|
||||||
public function run_maintenance(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'flush_cache':
|
|
||||||
// Flush WordPress object cache
|
|
||||||
wp_cache_flush();
|
|
||||||
// Clear transients
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_aksrt_%' OR option_name LIKE '_transient_timeout_aksrt_%'");
|
|
||||||
return $this->success(['message' => __('缓存已清除', 'aksrt')]);
|
|
||||||
|
|
||||||
case 'optimize_db':
|
|
||||||
global $wpdb;
|
|
||||||
$tables = [
|
|
||||||
$wpdb->prefix . 'aksrt_credit_log',
|
|
||||||
$wpdb->prefix . 'aksrt_signin_log',
|
|
||||||
$wpdb->prefix . 'aksrt_dl_token',
|
|
||||||
$wpdb->prefix . 'aksrt_notification',
|
|
||||||
];
|
|
||||||
$optimized = 0;
|
|
||||||
foreach ($tables as $table) {
|
|
||||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) {
|
|
||||||
$wpdb->query("OPTIMIZE TABLE {$table}");
|
|
||||||
$optimized++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return $this->error('未知的维护操作: ' . esc_html($action));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_update_info(): \WP_REST_Response {
|
public function get_update_info(): \WP_REST_Response {
|
||||||
$theme = wp_get_theme();
|
$theme = wp_get_theme();
|
||||||
$info = [
|
$info = [
|
||||||
@@ -199,32 +92,4 @@ class ToolsController extends ControllerBase {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rest_email_rollback_list(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
|
||||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$page = max(1, (int) $request->get_param('page'));
|
|
||||||
$per_page = min(100, max(1, (int) $request->get_param('per_page')));
|
|
||||||
$data = \AKSRT\Auth\EmailRollback::list_pending($page, $per_page);
|
|
||||||
|
|
||||||
return $this->success(['data' => $data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rest_email_rollback_execute(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
|
||||||
return $this->error(__('邮箱回滚功能不可用', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user_id = (int) $request->get_param('id');
|
|
||||||
$result = \AKSRT\Auth\EmailRollback::rollback($user_id, get_current_user_id());
|
|
||||||
if (empty($result['success'])) {
|
|
||||||
return $this->error((string) ($result['error'] ?? __('邮箱回滚失败', 'aksrt')), 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'message' => (string) ($result['message'] ?? __('邮箱已回滚', 'aksrt')),
|
|
||||||
'data' => $result,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ trait UserControllerProfileMediaOperations {
|
|||||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
$capability = $is_avatar ? 'upload_avatar' : 'upload_profile_background';
|
$upload_enabled = $is_avatar
|
||||||
if (!function_exists('aksrt_user_can') || !aksrt_user_can($capability, $user_id)) {
|
? function_exists('aksrt_is_avatar_upload_enabled') && aksrt_is_avatar_upload_enabled()
|
||||||
|
: function_exists('aksrt_is_user_cover_upload_enabled') && aksrt_is_user_cover_upload_enabled();
|
||||||
|
if (!$upload_enabled) {
|
||||||
return $this->error($is_avatar
|
return $this->error($is_avatar
|
||||||
? __('当前账号没有上传头像的权限', 'aksrt')
|
? __('当前账号没有上传头像的权限', 'aksrt')
|
||||||
: __('当前账号没有上传用户资料卡背景图的权限', 'aksrt'), 403);
|
: __('当前账号没有上传用户资料卡背景图的权限', 'aksrt'), 403);
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package AKSRT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace AKSRT\Auth;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class EmailRollback {
|
|
||||||
|
|
||||||
const META_PREVIOUS = 'aksrt_email_previous';
|
|
||||||
const META_CHANGED_AT = 'aksrt_email_changed_at';
|
|
||||||
const WINDOW_SECONDS = 1209600; // 14 days
|
|
||||||
|
|
||||||
private static $instance = null;
|
|
||||||
|
|
||||||
private function __construct() {}
|
|
||||||
|
|
||||||
public static function get_instance(): self {
|
|
||||||
if (null === self::$instance) self::$instance = new self();
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function init(): void {
|
|
||||||
add_filter('user_row_actions', [$this, 'add_row_action'], 20, 2);
|
|
||||||
add_action('admin_post_aksrt_email_rollback', [$this, 'handle_admin_rollback']);
|
|
||||||
// cron 清理过期 meta
|
|
||||||
add_action('aksrt_email_rollback_cleanup', [self::class, 'cron_cleanup']);
|
|
||||||
add_action('wp', [self::class, 'maybe_schedule_cleanup']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function maybe_schedule_cleanup(): void {
|
|
||||||
if (!wp_next_scheduled('aksrt_email_rollback_cleanup')) {
|
|
||||||
wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'aksrt_email_rollback_cleanup');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function cron_cleanup(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$cutoff = time() - self::WINDOW_SECONDS;
|
|
||||||
$rows = $wpdb->get_col($wpdb->prepare(
|
|
||||||
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) < %d",
|
|
||||||
self::META_CHANGED_AT,
|
|
||||||
$cutoff
|
|
||||||
));
|
|
||||||
if (empty($rows)) return;
|
|
||||||
foreach ($rows as $uid) {
|
|
||||||
delete_user_meta((int) $uid, self::META_PREVIOUS);
|
|
||||||
delete_user_meta((int) $uid, self::META_CHANGED_AT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在 14 天窗口内?
|
|
||||||
*/
|
|
||||||
public static function is_within_window(int $user_id): bool {
|
|
||||||
$changed_at = (int) get_user_meta($user_id, self::META_CHANGED_AT, true);
|
|
||||||
if ($changed_at <= 0) return false;
|
|
||||||
return (time() - $changed_at) <= self::WINDOW_SECONDS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出窗口内可回滚的用户。
|
|
||||||
*/
|
|
||||||
public static function list_pending(int $page = 1, int $per_page = 20): array {
|
|
||||||
global $wpdb;
|
|
||||||
$page = max(1, $page);
|
|
||||||
$per_page = min(100, max(1, $per_page));
|
|
||||||
$offset = ($page - 1) * $per_page;
|
|
||||||
$cutoff = time() - self::WINDOW_SECONDS;
|
|
||||||
|
|
||||||
$sql_count = $wpdb->prepare(
|
|
||||||
"SELECT COUNT(DISTINCT um1.user_id)
|
|
||||||
FROM {$wpdb->usermeta} um1
|
|
||||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
|
||||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d",
|
|
||||||
self::META_PREVIOUS,
|
|
||||||
self::META_CHANGED_AT,
|
|
||||||
$cutoff
|
|
||||||
);
|
|
||||||
$total = (int) $wpdb->get_var($sql_count);
|
|
||||||
|
|
||||||
$sql = $wpdb->prepare(
|
|
||||||
"SELECT um1.user_id, um1.meta_value AS changed_at, um2.meta_value AS previous_email
|
|
||||||
FROM {$wpdb->usermeta} um1
|
|
||||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
|
||||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d
|
|
||||||
ORDER BY CAST(um1.meta_value AS UNSIGNED) DESC
|
|
||||||
LIMIT %d OFFSET %d",
|
|
||||||
self::META_PREVIOUS,
|
|
||||||
self::META_CHANGED_AT,
|
|
||||||
$cutoff,
|
|
||||||
$per_page,
|
|
||||||
$offset
|
|
||||||
);
|
|
||||||
$rows = $wpdb->get_results($sql, ARRAY_A);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ((array) $rows as $r) {
|
|
||||||
$uid = (int) $r['user_id'];
|
|
||||||
$u = get_userdata($uid);
|
|
||||||
if (!$u) continue;
|
|
||||||
$items[] = [
|
|
||||||
'user_id' => $uid,
|
|
||||||
'login' => $u->user_login,
|
|
||||||
'display_name' => $u->display_name,
|
|
||||||
'current_email' => $u->user_email,
|
|
||||||
'previous_email' => (string) $r['previous_email'],
|
|
||||||
'changed_at' => (int) $r['changed_at'],
|
|
||||||
'expires_in' => max(0, (int) $r['changed_at'] + self::WINDOW_SECONDS - time()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'items' => $items,
|
|
||||||
'total' => $total,
|
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $per_page,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行回滚。
|
|
||||||
*
|
|
||||||
* @return array{success:bool, message?:string, error?:string}
|
|
||||||
*/
|
|
||||||
public static function rollback(int $user_id, int $by_admin_id = 0): array {
|
|
||||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
|
||||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
|
||||||
}
|
|
||||||
if (!self::is_within_window($user_id)) {
|
|
||||||
return ['success' => false, 'error' => __('回滚窗口已过期或当前账号未发起过邮箱变更', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$previous = (string) get_user_meta($user_id, self::META_PREVIOUS, true);
|
|
||||||
if ($previous === '' || !is_email($previous)) {
|
|
||||||
return ['success' => false, 'error' => __('旧邮箱无效', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查旧邮箱是否被其他用户占用(理论上极少见)
|
|
||||||
$occupier = email_exists($previous);
|
|
||||||
if ($occupier && (int) $occupier !== $user_id) {
|
|
||||||
return ['success' => false, 'error' => __('旧邮箱已被其他账号占用,无法回滚', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$update = wp_update_user([
|
|
||||||
'ID' => $user_id,
|
|
||||||
'user_email' => $previous,
|
|
||||||
]);
|
|
||||||
if (is_wp_error($update)) {
|
|
||||||
return ['success' => false, 'error' => $update->get_error_message()];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空回滚 meta(一次性)
|
|
||||||
delete_user_meta($user_id, self::META_PREVIOUS);
|
|
||||||
delete_user_meta($user_id, self::META_CHANGED_AT);
|
|
||||||
|
|
||||||
// 记录管理员操作日志
|
|
||||||
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
|
|
||||||
if (!is_array($log)) $log = [];
|
|
||||||
$log[] = [
|
|
||||||
't' => time(),
|
|
||||||
'action' => 'email_rollback',
|
|
||||||
'by' => $by_admin_id ?: get_current_user_id(),
|
|
||||||
];
|
|
||||||
if (count($log) > 10) $log = array_slice($log, -10);
|
|
||||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在用户列表行操作中加入"回滚邮箱"。
|
|
||||||
*/
|
|
||||||
public function add_row_action(array $actions, \WP_User $user): array {
|
|
||||||
if (!current_user_can('edit_user', $user->ID)) return $actions;
|
|
||||||
if (!self::is_within_window((int) $user->ID)) return $actions;
|
|
||||||
|
|
||||||
$url = wp_nonce_url(
|
|
||||||
admin_url('admin-post.php?action=aksrt_email_rollback&user_id=' . (int) $user->ID),
|
|
||||||
'aksrt_email_rollback_' . (int) $user->ID
|
|
||||||
);
|
|
||||||
$actions['aksrt_email_rollback'] = sprintf(
|
|
||||||
'<a href="%s">%s</a>',
|
|
||||||
esc_url($url),
|
|
||||||
esc_html__('回滚邮箱', 'aksrt')
|
|
||||||
);
|
|
||||||
return $actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle_admin_rollback(): void {
|
|
||||||
$user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
|
|
||||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
|
||||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
|
||||||
}
|
|
||||||
check_admin_referer('aksrt_email_rollback_' . $user_id);
|
|
||||||
|
|
||||||
$r = self::rollback($user_id, get_current_user_id());
|
|
||||||
$type = !empty($r['success']) ? 'success' : 'error';
|
|
||||||
$msg = $r['message'] ?? $r['error'] ?? '';
|
|
||||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [['type' => $type, 'msg' => $msg]], 30);
|
|
||||||
|
|
||||||
wp_safe_redirect(wp_get_referer() ?: admin_url('users.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -543,11 +543,6 @@ trait ScriptsPart1c {
|
|||||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||||
// Alias used by modal-auth / follow / report (same value as restUrl).
|
// Alias used by modal-auth / follow / report (same value as restUrl).
|
||||||
'apiUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
'apiUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||||
'frontendModalRollout' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_frontend_modal_rollout() : [
|
|
||||||
'vip' => true,
|
|
||||||
'order' => true,
|
|
||||||
'identity' => true,
|
|
||||||
],
|
|
||||||
'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s']) : true,
|
'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s']) : true,
|
||||||
'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s'] : ['home', 'cat'],
|
'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s'] : ['home', 'cat'],
|
||||||
'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['list_badge_show']) : true,
|
'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['list_badge_show']) : true,
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ class Capabilities {
|
|||||||
'comment_delete_my_post' => ['label' => __('删除自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
'comment_delete_my_post' => ['label' => __('删除自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
||||||
'comment_delete_other' => ['label' => __('删除其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
'comment_delete_other' => ['label' => __('删除其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
||||||
|
|
||||||
'upload_avatar' => ['label' => __('上传头像', 'aksrt'), 'default' => ['logged' => true]],
|
|
||||||
'upload_profile_background' => ['label' => __('上传用户资料卡背景图', 'aksrt'), 'default' => ['logged' => true]],
|
|
||||||
'upload_submit_image' => ['label' => __('投稿图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
'upload_submit_image' => ['label' => __('投稿图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||||
'upload_comment_image' => ['label' => __('评论图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
'upload_comment_image' => ['label' => __('评论图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||||
'upload_resource_attachment'=> ['label' => __('资源附件上传', 'aksrt'), 'exclude_roles' => ['all']],
|
'upload_resource_attachment'=> ['label' => __('资源附件上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||||
@@ -153,36 +151,4 @@ class Capabilities {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取"系统工具"设置
|
|
||||||
*/
|
|
||||||
public static function get_system_tools_settings(): array {
|
|
||||||
$s = Settings::get_all();
|
|
||||||
$time_format = sanitize_text_field((string) ($s['time_format'] ?? 'n月j日 H:i'));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'remove_emoji' => !empty($s['remove_emoji'] ?? true),
|
|
||||||
'remove_open_sans' => !empty($s['remove_open_sans'] ?? true),
|
|
||||||
'remove_more_wp_head' => !empty($s['remove_more_wp_head'] ?? true),
|
|
||||||
'hide_admin_bar' => !empty($s['hide_admin_bar'] ?? true),
|
|
||||||
'remove_dashboard_widgets' => !empty($s['remove_dashboard_widgets'] ?? true),
|
|
||||||
'admin_frontend_set' => !empty($s['admin_frontend_set'] ?? true),
|
|
||||||
'display_wp_update' => !empty($s['display_wp_update'] ?? true),
|
|
||||||
'close_gutenberg' => !empty($s['close_gutenberg']),
|
|
||||||
'admin_user_del_fields' => !empty($s['admin_user_del_fields'] ?? true),
|
|
||||||
'split_upload_s' => !empty($s['split_upload_s'] ?? true),
|
|
||||||
'split_minimum_size' => max(4, min(100, (int) ($s['split_minimum_size'] ?? 20))),
|
|
||||||
'newfilename' => !empty($s['newfilename']),
|
|
||||||
'newfilename_type' => in_array($s['newfilename_type'] ?? 'time', ['random', 'time'], true)
|
|
||||||
? $s['newfilename_type'] : 'time',
|
|
||||||
'disabled_large_thumbnail' => !empty($s['disabled_large_thumbnail'] ?? true),
|
|
||||||
'disabled_autoembed' => !empty($s['disabled_autoembed'] ?? true),
|
|
||||||
'mce_float_toolbar' => [
|
|
||||||
'pc_s' => !empty($s['mce_float_toolbar_pc_s'] ?? true),
|
|
||||||
'm_s' => !empty($s['mce_float_toolbar_m_s'] ?? true),
|
|
||||||
],
|
|
||||||
'time_ago_s' => !empty($s['time_ago_s'] ?? true),
|
|
||||||
'time_format' => $time_format,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,11 +146,4 @@ trait DomainGroups {
|
|||||||
return Capabilities::user_can($cap, $user_id, $context);
|
return Capabilities::user_can($cap, $user_id, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取"系统工具"设置
|
|
||||||
*/
|
|
||||||
public static function get_system_tools_settings(): array {
|
|
||||||
return Capabilities::get_system_tools_settings();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Frontend modal runtime settings.
|
|
||||||
*
|
|
||||||
* @package AKSRT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace AKSRT\Settings;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class FrontendModalRuntime {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the per-domain frontend modal rollout switches.
|
|
||||||
*
|
|
||||||
* @return array{vip:bool,order:bool,identity:bool}
|
|
||||||
*/
|
|
||||||
public static function get_frontend_modal_rollout(): array {
|
|
||||||
$settings = Settings::get_all();
|
|
||||||
|
|
||||||
return [
|
|
||||||
// Canonical-only flows must be live when the option has never been configured.
|
|
||||||
// An explicit false remains an intentional per-domain kill switch.
|
|
||||||
'vip' => !array_key_exists('frontend_modal_rollout_vip', $settings) || !empty($settings['frontend_modal_rollout_vip']),
|
|
||||||
'order' => !array_key_exists('frontend_modal_rollout_order', $settings) || !empty($settings['frontend_modal_rollout_order']),
|
|
||||||
'identity' => !array_key_exists('frontend_modal_rollout_identity', $settings) || !empty($settings['frontend_modal_rollout_identity']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-11
@@ -513,17 +513,6 @@ class Settings {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取前台弹窗按领域灰度开关。
|
|
||||||
*
|
|
||||||
* @return array{vip:bool,order:bool,identity:bool}
|
|
||||||
*/
|
|
||||||
public static function get_frontend_modal_rollout(): array {
|
|
||||||
return FrontendModalRuntime::get_frontend_modal_rollout();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取侧边栏配置
|
* 获取侧边栏配置
|
||||||
*
|
*
|
||||||
@@ -557,6 +546,17 @@ class Settings {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical avatar, cover, and signature settings.
|
||||||
|
*
|
||||||
|
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
|
||||||
|
*/
|
||||||
|
public static function get_user_feature_settings(): array {
|
||||||
|
return UserRuntime::get_user_feature_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private user-center display settings.
|
* Private user-center display settings.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -9,6 +9,31 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
class UserRuntime {
|
class UserRuntime {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical avatar, cover, and signature settings.
|
||||||
|
*
|
||||||
|
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
|
||||||
|
*/
|
||||||
|
public static function get_user_feature_settings(): array {
|
||||||
|
$settings = Settings::read_domain('users', 'features');
|
||||||
|
$source = sanitize_key((string) ($settings['avatar_external'] ?? ''));
|
||||||
|
if (!in_array($source, ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'], true)) {
|
||||||
|
$source = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = esc_url_raw((string) ($settings['avatar_default_img'] ?? ''));
|
||||||
|
$cover = esc_url_raw((string) ($settings['user_cover_img'] ?? ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'avatar_upload_s' => !array_key_exists('avatar_upload_s', $settings) || !empty($settings['avatar_upload_s']),
|
||||||
|
'avatar_external' => $source,
|
||||||
|
'avatar_default_img' => $avatar !== '' ? $avatar : get_theme_file_uri('assets/img/avatar-default.png'),
|
||||||
|
'user_cover_upload_s' => !array_key_exists('user_cover_upload_s', $settings) || !empty($settings['user_cover_upload_s']),
|
||||||
|
'user_cover_img' => $cover !== '' ? $cover : get_theme_file_uri('assets/img/user_t.jpg'),
|
||||||
|
'user_desc_std' => sanitize_text_field((string) ($settings['user_desc_std'] ?? '这家伙很懒,什么都没有写...')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public user-home display settings.
|
* Public user-home display settings.
|
||||||
*
|
*
|
||||||
@@ -71,56 +96,8 @@ class UserRuntime {
|
|||||||
* @return array{tabs:string[]}
|
* @return array{tabs:string[]}
|
||||||
*/
|
*/
|
||||||
public static function get_user_center_settings(): array {
|
public static function get_user_center_settings(): array {
|
||||||
$settings = Settings::get_all();
|
|
||||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,comments,credits,revenue,favorites,auth';
|
|
||||||
$tabs = is_array($raw_tabs)
|
|
||||||
? $raw_tabs
|
|
||||||
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
|
|
||||||
|
|
||||||
// Built-in switchable panels. VIP is always auto-injected when enabled
|
|
||||||
// by AKSRT\User\Center — keep it allowed for explicit config too.
|
|
||||||
$allowed_tabs = ['profile', 'level', 'comments', 'favorites', 'credits', 'revenue', 'vip', 'orders', 'auth', 'settings'];
|
|
||||||
$normalized_tabs = ['profile'];
|
|
||||||
|
|
||||||
foreach ((array) $tabs as $tab) {
|
|
||||||
$tab = sanitize_key((string) $tab);
|
|
||||||
if ($tab === 'profile') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($tab, $allowed_tabs, true) && !in_array($tab, $normalized_tabs, true)) {
|
|
||||||
$normalized_tabs[] = $tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array('comments', $normalized_tabs, true)) {
|
|
||||||
$normalized_tabs[] = 'comments';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure VIP appears in configured tabs when membership is on, even if
|
|
||||||
// the stored option string is older and omits it.
|
|
||||||
$vip_on = !empty(Settings::get_payment_vip_settings()['enabled']);
|
|
||||||
if ($vip_on && !in_array('vip', $normalized_tabs, true)) {
|
|
||||||
// Prefer after credits, else after favorites, else append before settings.
|
|
||||||
$insert_at = count($normalized_tabs);
|
|
||||||
foreach (['credits', 'favorites', 'posts'] as $anchor) {
|
|
||||||
$idx = array_search($anchor, $normalized_tabs, true);
|
|
||||||
if ($idx !== false) {
|
|
||||||
$insert_at = (int) $idx + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
array_splice($normalized_tabs, $insert_at, 0, ['vip']);
|
|
||||||
}
|
|
||||||
if (!$vip_on) {
|
|
||||||
$normalized_tabs = array_values(array_filter(
|
|
||||||
$normalized_tabs,
|
|
||||||
static fn(string $t): bool => $t !== 'vip'
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tabs' => $normalized_tabs,
|
'tabs' => ['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ final class UserRenderer {
|
|||||||
$card_classes = 'aksrt-user-card is-guest';
|
$card_classes = 'aksrt-user-card is-guest';
|
||||||
$cover = '';
|
$cover = '';
|
||||||
if (!empty($i['show_cover'])) {
|
if (!empty($i['show_cover'])) {
|
||||||
$cover = '<div class="aksrt-user-card__cover"><img' . aksrt_get_lazy_attr('lazy_cover', get_theme_file_uri('assets/img/person_background.png'), 'lazyload fit-cover', aksrt_get_lazy_thumb('lg')) . ' alt=""></div>';
|
$cover_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url(0) : get_theme_file_uri('assets/img/user_t.jpg');
|
||||||
|
$cover = '<div class="aksrt-user-card__cover"><img' . aksrt_get_lazy_attr('lazy_cover', $cover_url, 'lazyload fit-cover', aksrt_get_lazy_thumb('lg')) . ' alt=""></div>';
|
||||||
} else {
|
} else {
|
||||||
$card_classes .= ' no-cover';
|
$card_classes .= ' no-cover';
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,8 @@ final class UserRenderer {
|
|||||||
private static function profile_card(\WP_User $user, array $i, bool $self_card): string {
|
private static function profile_card(\WP_User $user, array $i, bool $self_card): string {
|
||||||
$user_id = (int) $user->ID;
|
$user_id = (int) $user->ID;
|
||||||
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
|
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
|
||||||
$cover = (string) get_user_meta($user_id, 'aksrt_background_image', true);
|
$cover = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
|
||||||
|
$description = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : (string) $user->description;
|
||||||
$followers = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id) : 0;
|
$followers = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id) : 0;
|
||||||
$following = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_following_count($user_id) : 0;
|
$following = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_following_count($user_id) : 0;
|
||||||
$html = '<article class="aksrt-user-card' . (!$self_card ? ' is-author' : '') . (empty($i['show_cover']) ? ' no-cover' : '') . '">';
|
$html = '<article class="aksrt-user-card' . (!$self_card ? ' is-author' : '') . (empty($i['show_cover']) ? ' no-cover' : '') . '">';
|
||||||
@@ -94,7 +96,7 @@ final class UserRenderer {
|
|||||||
if (function_exists('aksrt_render_user_identity_chips')) {
|
if (function_exists('aksrt_render_user_identity_chips')) {
|
||||||
$html .= '<div class="aksrt-user-card__identity">' . aksrt_render_user_identity_chips($user_id, 'widget') . '</div>';
|
$html .= '<div class="aksrt-user-card__identity">' . aksrt_render_user_identity_chips($user_id, 'widget') . '</div>';
|
||||||
}
|
}
|
||||||
$html .= '<p>' . esc_html(wp_trim_words((string) $user->description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
|
$html .= '<p>' . esc_html(wp_trim_words($description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
|
||||||
if (!empty($i['show_buttons'])) {
|
if (!empty($i['show_buttons'])) {
|
||||||
$html .= '<div class="aksrt-user-card__actions">' . self::action((string) $i['primary_action'], $user, true) . self::action((string) $i['secondary_action'], $user, false) . '</div>';
|
$html .= '<div class="aksrt-user-card__actions">' . self::action((string) $i['primary_action'], $user, true) . self::action((string) $i['secondary_action'], $user, false) . '</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ $is_self = is_user_logged_in() && get_current_user_id() === $author_id;
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<?php if ($show_bio) : ?>
|
<?php if ($show_bio) : ?>
|
||||||
<?php $bio = trim((string) get_the_author_meta('description')); ?>
|
<?php $bio = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($author_id) : trim((string) get_the_author_meta('description')); ?>
|
||||||
<?php if ($bio !== '') : ?>
|
<?php if ($bio !== '') : ?>
|
||||||
<span class="icon-circle"><?php echo esc_html($bio); ?></span>
|
<span class="icon-circle"><?php echo esc_html($bio); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ $has_more_details = $registered_time !== '' || $last_login_time !== '' || !empty
|
|||||||
$signals = $dashboard['profile_signals'] ?? [];
|
$signals = $dashboard['profile_signals'] ?? [];
|
||||||
$notice = $dashboard['profile_notice'] ?? null;
|
$notice = $dashboard['profile_notice'] ?? null;
|
||||||
$show_report = !empty($dashboard['show_report']);
|
$show_report = !empty($dashboard['show_report']);
|
||||||
$bg_url = $dashboard['background_url'] ?? '';
|
$bg_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
|
||||||
$is_own = !empty($dashboard['is_own_profile']);
|
$is_own = !empty($dashboard['is_own_profile']);
|
||||||
|
|
||||||
$follower_count = (int) ($tab_counts['follow'] ?? 0);
|
$follower_count = (int) ($tab_counts['follow'] ?? 0);
|
||||||
@@ -60,7 +60,7 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0
|
|||||||
?><div class="aksrt-author-hero<?php echo $show_cover ? '' : ' aksrt-author-hero--no-cover'; ?>">
|
?><div class="aksrt-author-hero<?php echo $show_cover ? '' : ' aksrt-author-hero--no-cover'; ?>">
|
||||||
<?php if ($show_cover) : ?>
|
<?php if ($show_cover) : ?>
|
||||||
<div class="aksrt-author-hero__cover">
|
<div class="aksrt-author-hero__cover">
|
||||||
<img<?php echo aksrt_get_lazy_attr('lazy_cover', (string) ($bg_url ? $bg_url : get_theme_file_uri('assets/img/person_background.png')), 'aksrt-author-hero__cover-img', aksrt_get_lazy_thumb('lg')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> alt="">
|
<img<?php echo aksrt_get_lazy_attr('lazy_cover', (string) $bg_url, 'aksrt-author-hero__cover-img', aksrt_get_lazy_thumb('lg')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> alt="">
|
||||||
<div class="aksrt-author-hero__cover-mask"></div>
|
<div class="aksrt-author-hero__cover-mask"></div>
|
||||||
<?php if ($follower_count > 0) : ?>
|
<?php if ($follower_count > 0) : ?>
|
||||||
<div class="aksrt-author-hero__cover-metas" aria-label="<?php esc_attr_e('主页数据', 'aksrt'); ?>">
|
<div class="aksrt-author-hero__cover-metas" aria-label="<?php esc_attr_e('主页数据', 'aksrt'); ?>">
|
||||||
@@ -124,8 +124,9 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0
|
|||||||
<?php echo $city_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
<?php echo $city_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($show_desc && $user->description) : ?>
|
<?php $user_description = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : (string) $user->description; ?>
|
||||||
<p class="aksrt-author-hero__desc"><?php echo esc_html($user->description); ?></p>
|
<?php if ($show_desc && $user_description !== '') : ?>
|
||||||
|
<p class="aksrt-author-hero__desc"><?php echo esc_html($user_description); ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($notice) && is_array($notice)) : ?>
|
<?php if (!empty($notice) && is_array($notice)) : ?>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ $avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($use
|
|||||||
$home_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
|
$home_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
|
||||||
$credits = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
$credits = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
||||||
$post_count = (int) count_user_posts($user_id, 'post', true);
|
$post_count = (int) count_user_posts($user_id, 'post', true);
|
||||||
$bio = (string) get_the_author_meta('description', $user_id);
|
$bio = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : (string) get_the_author_meta('description', $user_id);
|
||||||
$show_credits = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::credits_enabled() : true;
|
$show_credits = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::credits_enabled() : true;
|
||||||
?>
|
?>
|
||||||
<div class="aksrt-profile-card aksrt-profile-card--shortcode">
|
<div class="aksrt-profile-card aksrt-profile-card--shortcode">
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
|
||||||
|
test('badges use server-rendered admin-ajax modal actions only', () => {
|
||||||
|
const badges = read('src/User/Badges.php');
|
||||||
|
const identity = read('inc/helpers/aksrt-identity.php');
|
||||||
|
const scripts = read('src/Core/ScriptsPart1b.php');
|
||||||
|
|
||||||
|
for (const action of [
|
||||||
|
'aksrt_user_medal_info_modal',
|
||||||
|
'aksrt_single_medal_info_modal',
|
||||||
|
'aksrt_user_medal_manually_add_modal',
|
||||||
|
'aksrt_user_medal_wear',
|
||||||
|
'aksrt_user_medal_manually_add',
|
||||||
|
'aksrt_user_medal_manually_remove',
|
||||||
|
]) {
|
||||||
|
assert.match(badges, new RegExp(`wp_ajax_${action}`));
|
||||||
|
}
|
||||||
|
assert.match(badges, /wp_ajax_nopriv_aksrt_user_medal_info_modal/);
|
||||||
|
assert.match(badges, /wp_ajax_nopriv_aksrt_single_medal_info_modal/);
|
||||||
|
assert.match(identity, /data-aksrt-modal-cache="false"/);
|
||||||
|
assert.match(identity, /admin-ajax\.php/);
|
||||||
|
assert.match(identity, /aksrt-badge-modal--overview/);
|
||||||
|
assert.match(identity, /aksrt-badge-modal--grant/);
|
||||||
|
assert.match(badges, /aksrt-badge-modal--single/);
|
||||||
|
assert.doesNotMatch(identity, /data-aksrt-modal-class="full-sm"/);
|
||||||
|
assert.doesNotMatch(badges, /data-aksrt-modal-class="modal-mini"/);
|
||||||
|
assert.match(scripts, /user-badge-actions\.js/);
|
||||||
|
assert.doesNotMatch(scripts, /user-badges\.js/);
|
||||||
|
assert.doesNotMatch(badges, /register_rest_route|rest_wear|rest_list|rest_grant|rest_revoke/);
|
||||||
|
assert.doesNotMatch(identity, /data-aksrt-badges-open|data-aksrt-badges-can-wear/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('badge mutations enforce the public nonce, login, ownership, and manual grant rules', () => {
|
||||||
|
const badges = read('src/User/Badges.php');
|
||||||
|
|
||||||
|
assert.match(badges, /ajax_nonce\(\): string \{ return 'aksrt_public_nonce'; \}/);
|
||||||
|
assert.match(badges, /wp_verify_nonce\(\$nonce, self::ajax_nonce\(\)\)/);
|
||||||
|
assert.match(badges, /if \(!is_user_logged_in\(\)\) self::ajax_error/);
|
||||||
|
assert.match(badges, /if \(\$operation !== 'wear' && !self::can_manually_set\(\)\)/);
|
||||||
|
assert.match(badges, /\$operation === 'grant' && \(\$item\['get_type'\] \?\? ''\) !== 'manually_add'/);
|
||||||
|
assert.match(badges, /\$operation === 'revoke' && empty\(\$item\['owned'\]\)/);
|
||||||
|
assert.match(badges, /if \(!in_array\(\$slug, \$owned, true\)\)/);
|
||||||
|
assert.match(badges, /'show_unearned'\s*=>\s*\$show_unearned/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('badge actions close the HTML dialog and reload the server-rendered page', () => {
|
||||||
|
const actions = read('assets/js/user-badge-actions.js');
|
||||||
|
|
||||||
|
assert.match(actions, /aksrt_user_medal_wear/);
|
||||||
|
assert.match(actions, /aksrt_user_medal_manually_add/);
|
||||||
|
assert.match(actions, /aksrt_user_medal_manually_remove/);
|
||||||
|
assert.match(actions, /AksrtModalRuntime\.close\('badge-action'\)/);
|
||||||
|
assert.match(actions, /window\.location\.reload\(\)/);
|
||||||
|
assert.doesNotMatch(actions, /AksrtRequest|fetchJson|restUrl/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the retired badge rollout and JSON modal CSS are absent', () => {
|
||||||
|
const retired = [
|
||||||
|
'src/Settings/FrontendModalRuntime.php',
|
||||||
|
'src/Admin/Schema/SchemaToolsFrontendModalRollout.php',
|
||||||
|
'assets/admin/src/schema/sections/tools/frontend-modal-rollout.js',
|
||||||
|
];
|
||||||
|
for (const file of retired) {
|
||||||
|
assert.equal(fs.existsSync(path.join(root, file)), false, `${file} should be deleted`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = [
|
||||||
|
'src/Core/ScriptsPart1c.php',
|
||||||
|
'assets/css/frontend/medal.css',
|
||||||
|
].map(read).join('\n');
|
||||||
|
|
||||||
|
assert.doesNotMatch(sources, /frontend_modal_rollout_badge/);
|
||||||
|
assert.doesNotMatch(sources, /\.uc-badges-/);
|
||||||
|
assert.match(sources, /grid-template-columns:\s*repeat\(5, 1fr\)/);
|
||||||
|
assert.match(sources, /grid-template-columns:\s*repeat\(4, 1fr\)/);
|
||||||
|
assert.match(sources, /grid-template-columns:\s*repeat\(3, 1fr\)/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
const exists = (file) => fs.existsSync(path.join(root, file));
|
||||||
|
|
||||||
|
const expectedFields = [
|
||||||
|
'avatar_upload_s',
|
||||||
|
'avatar_external',
|
||||||
|
'avatar_default_img',
|
||||||
|
'user_cover_upload_s',
|
||||||
|
'user_cover_img',
|
||||||
|
'user_desc_std',
|
||||||
|
];
|
||||||
|
|
||||||
|
const externalSources = ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'];
|
||||||
|
|
||||||
|
test('user features schemas expose exactly the six canonical fields', () => {
|
||||||
|
const js = read('assets/admin/src/schema/sections/users/features.js');
|
||||||
|
const phpAll = read('src/Admin/Schema/SchemaUsers.php');
|
||||||
|
const php = phpAll.slice(phpAll.indexOf("'features' => ["), phpAll.indexOf("'verify' => ["));
|
||||||
|
|
||||||
|
const jsFields = [...js.matchAll(/\{\s*key:\s*'([^']+)',\s*label:/g)].map((match) => match[1]);
|
||||||
|
const phpFields = [...php.matchAll(/\['key'\s*=>\s*'([^']+)',\s*'label'/g)].map((match) => match[1]);
|
||||||
|
assert.deepEqual(jsFields, expectedFields);
|
||||||
|
assert.deepEqual(phpFields, expectedFields);
|
||||||
|
|
||||||
|
assert.match(js, /avatar_upload_s:\s*true/);
|
||||||
|
assert.match(js, /avatar_external:\s*''/);
|
||||||
|
assert.match(js, /assets\/img\/avatar-default\.png/);
|
||||||
|
assert.match(js, /user_cover_upload_s:\s*true/);
|
||||||
|
assert.match(js, /assets\/img\/user_t\.jpg/);
|
||||||
|
assert.match(js, /user_desc_std:\s*'这家伙很懒,什么都没有写\.\.\.'/);
|
||||||
|
assert.match(php, /avatar_default_img[^\n]+get_theme_file_uri\('assets\/img\/avatar-default\.png'\)/);
|
||||||
|
assert.match(php, /user_cover_img[^\n]+get_theme_file_uri\('assets\/img\/user_t\.jpg'\)/);
|
||||||
|
|
||||||
|
const jsSources = [...js.matchAll(/\{ label: '[^']+', value: '([^']*)' \}/g)].map((match) => match[1]);
|
||||||
|
const phpSources = [...php.matchAll(/\['label' => '[^']+', 'value' => '([^']*)'\]/g)].map((match) => match[1]);
|
||||||
|
assert.deepEqual(jsSources, externalSources);
|
||||||
|
assert.deepEqual(phpSources, externalSources);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('avatar, cover, signature, and upload runtime use the canonical settings', () => {
|
||||||
|
const helpers = read('inc/helpers/aksrt-avatar.php');
|
||||||
|
const runtime = read('src/Settings/UserRuntime.php');
|
||||||
|
const upload = read('src/Admin/UserControllerProfileMediaOperations.php');
|
||||||
|
const center = read('inc/helpers/aksrt-user-center.php');
|
||||||
|
|
||||||
|
const customAt = helpers.indexOf("$custom = aksrt_get_custom_avatar_url");
|
||||||
|
const externalAt = helpers.indexOf("$external = aksrt_get_external_avatar_url", customAt);
|
||||||
|
const defaultAt = helpers.indexOf('return aksrt_get_default_avatar_url();', externalAt);
|
||||||
|
assert.ok(customAt >= 0 && customAt < externalAt && externalAt < defaultAt);
|
||||||
|
|
||||||
|
const hosts = {
|
||||||
|
gravatar: 'https://www.gravatar.com/avatar/',
|
||||||
|
weavatar: 'https://weavatar.com/avatar/',
|
||||||
|
cravatar: 'https://cn.cravatar.com/avatar/',
|
||||||
|
geekzu: 'https://sdn.geekzu.org/avatar/',
|
||||||
|
loli: 'https://gravatar.loli.net/avatar/',
|
||||||
|
sep: 'https://cdn.sep.cc/avatar/',
|
||||||
|
v2ex: 'https://cdn.v2ex.com/gravatar/',
|
||||||
|
};
|
||||||
|
for (const [source, host] of Object.entries(hosts)) {
|
||||||
|
assert.match(helpers, new RegExp(`'${source}'\\s*=>\\s*'${host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'`));
|
||||||
|
}
|
||||||
|
assert.match(helpers, /hash\('sha256', strtolower\(trim\(\$email\)\)\)/);
|
||||||
|
assert.match(helpers, /'s'\s*=>\s*\$size/);
|
||||||
|
assert.match(helpers, /'d'\s*=>\s*aksrt_get_default_avatar_url\(\)/);
|
||||||
|
|
||||||
|
assert.match(helpers, /get_user_meta\(\$user_id, 'aksrt_background_image', true\)/);
|
||||||
|
assert.match(helpers, /return \(string\) \$settings\['user_cover_img'\]/);
|
||||||
|
assert.match(helpers, /get_user_meta\(\$user_id, 'description', true\)/);
|
||||||
|
assert.match(helpers, /return \(string\) \$settings\['user_desc_std'\]/);
|
||||||
|
assert.match(helpers, /!empty\(\$settings\['avatar_upload_s'\]\)/);
|
||||||
|
assert.match(helpers, /!empty\(\$settings\['user_cover_upload_s'\]\)/);
|
||||||
|
|
||||||
|
assert.match(runtime, /Settings::read_domain\('users', 'features'\)/);
|
||||||
|
assert.doesNotMatch(runtime, /user_center_tabs/);
|
||||||
|
assert.match(runtime, /'tabs'\s*=>\s*\['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'\]/);
|
||||||
|
assert.match(upload, /aksrt_is_avatar_upload_enabled\(\)/);
|
||||||
|
assert.match(upload, /aksrt_is_user_cover_upload_enabled\(\)/);
|
||||||
|
assert.match(center, /'show_avatar_edit'\s*=>\s*\$allow_avatar_upload/);
|
||||||
|
assert.match(center, /'allow_bg_upload'\s*=>\s*\$allow_bg_upload/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('public user surfaces consume the canonical cover and signature helpers', () => {
|
||||||
|
for (const file of [
|
||||||
|
'templates/parts/user/author-hero.php',
|
||||||
|
'src/WidgetSystem/Renderers/UserRenderer.php',
|
||||||
|
'inc/helpers/aksrt-user-dashboard.php',
|
||||||
|
]) {
|
||||||
|
assert.match(read(file), /aksrt_get_user_cover_url/);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of [
|
||||||
|
'templates/parts/user/author-hero.php',
|
||||||
|
'src/WidgetSystem/Renderers/UserRenderer.php',
|
||||||
|
'inc/helpers/aksrt-graphic-card.php',
|
||||||
|
'inc/helpers/aksrt-single-render.php',
|
||||||
|
'templates/parts/single-author-bar.php',
|
||||||
|
'templates/shortcodes/profile.php',
|
||||||
|
'inc/helpers/header/aksrt-header-render.php',
|
||||||
|
]) {
|
||||||
|
assert.match(read(file), /aksrt_get_user_description/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retired admin pages, schemas, APIs, and metadata chains are absent', () => {
|
||||||
|
const deletedFiles = [
|
||||||
|
'assets/admin/src/pages/users/UsersFeatures.vue',
|
||||||
|
'assets/admin/src/pages/users/UsersSignin.vue',
|
||||||
|
'assets/admin/src/schema/sections/users/signin.js',
|
||||||
|
'assets/admin/src/pages/tools/ToolsEmailRollback.vue',
|
||||||
|
'assets/admin/src/schema/sections/tools/email-rollback.js',
|
||||||
|
'assets/admin/src/schema/sections/tools/frontend-modal-rollout.js',
|
||||||
|
'src/Admin/Schema/SchemaToolsEmailRollback.php',
|
||||||
|
'src/Admin/Schema/SchemaToolsFrontendModalRollout.php',
|
||||||
|
'src/Auth/EmailRollback.php',
|
||||||
|
'src/Settings/FrontendModalRuntime.php',
|
||||||
|
'assets/admin/src/pages/extensions/ExtensionTools.vue',
|
||||||
|
];
|
||||||
|
for (const file of deletedFiles) {
|
||||||
|
assert.equal(exists(file), false, `${file} should be deleted`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = [
|
||||||
|
'assets/admin/src/constants/sections/users.js',
|
||||||
|
'assets/admin/src/constants/sections/tools.js',
|
||||||
|
'assets/admin/src/constants/sections/extensions.js',
|
||||||
|
'assets/admin/src/router/component-loaders/users.js',
|
||||||
|
'assets/admin/src/router/component-loaders/tools.js',
|
||||||
|
'assets/admin/src/router/component-loaders/extensions.js',
|
||||||
|
'assets/admin/src/api/modules/tools.js',
|
||||||
|
'assets/admin/src/schema/sections/tools/index.js',
|
||||||
|
'src/Admin/Schema/SchemaTools.php',
|
||||||
|
'src/Admin/ToolsController.php',
|
||||||
|
'src/Settings/Settings.php',
|
||||||
|
'src/Settings/Capabilities.php',
|
||||||
|
'src/Settings/DomainGroups.php',
|
||||||
|
'src/Core/ScriptsPart1c.php',
|
||||||
|
'functions.php',
|
||||||
|
].map(read).join('\n');
|
||||||
|
|
||||||
|
for (const retired of [
|
||||||
|
'users/signin',
|
||||||
|
'email-rollback',
|
||||||
|
'EmailRollback',
|
||||||
|
'aksrt_email_previous',
|
||||||
|
'aksrt_email_changed_at',
|
||||||
|
'frontend_modal_rollout',
|
||||||
|
'FrontendModalRuntime',
|
||||||
|
'isRolloutEnabled',
|
||||||
|
'extensions/tools',
|
||||||
|
'/tools/maintenance',
|
||||||
|
'get_system_tools_settings',
|
||||||
|
'upload_avatar',
|
||||||
|
'upload_profile_background',
|
||||||
|
]) {
|
||||||
|
assert.equal(sources.includes(retired), false, `${retired} should be absent`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/credits/);
|
||||||
|
assert.doesNotMatch(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/signin/);
|
||||||
|
assert.match(read('assets/admin/src/api/modules/tools.js'), /\/settings\/health/);
|
||||||
|
assert.match(read('assets/admin/src/constants/sections/tools.js'), /route: '\/tools\/health'/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user