导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="AI 子系统" desc="配置 AI 内容辅助能力,包括 SEO 建议、摘要生成、标签建议等。" />
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<div class="aksrt-ai__status">
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.enabled ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
||||
<i :class="status.enabled ? 'fas fa-robot' : 'fas fa-robot'"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">AI 状态</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.enabled ? '已启用' : '已关闭' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.configured ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">API Key</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.configured ? '已配置' : '未配置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
||||
<i class="fas fa-brain"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">模型</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.model || '未设置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">可用状态</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>基本配置</h3>
|
||||
<div class="aksrt-form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.enabled"> 启用 AI 内容辅助
|
||||
</label>
|
||||
<p class="aksrt-form-hint">开启后可在投稿、编辑等场景使用 AI 辅助功能</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group">
|
||||
<label>AI Provider</label>
|
||||
<select v-model="form.provider" class="aksrt-select">
|
||||
<option value="none">不使用</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>API Key</label>
|
||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
||||
<label>API Base URL</label>
|
||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://api.openai.com/v1">
|
||||
<p class="aksrt-form-hint">可替换为兼容 OpenAI 接口的第三方服务</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>模型</label>
|
||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>最大 Token 数</label>
|
||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>每分钟请求限制</label>
|
||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
||||
</div>
|
||||
<div class="aksrt-form-actions">
|
||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
||||
{{ saving ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能说明 -->
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>支持的 AI 功能</h3>
|
||||
<table class="aksrt-table aksrt-table--sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>功能</th>
|
||||
<th>说明</th>
|
||||
<th>使用场景</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feat in features" :key="feat.key">
|
||||
<td>{{ feat.name }}</td>
|
||||
<td>{{ feat.desc }}</td>
|
||||
<td>{{ feat.scene }}</td>
|
||||
<td>
|
||||
<span class="aksrt-badge" :class="status.available ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
||||
{{ status.available ? '可用' : '未启用' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 重要提示 -->
|
||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
||||
<h3><i class="fas fa-exclamation-triangle"></i> 重要说明</h3>
|
||||
<ul>
|
||||
<li>AI 生成的内容仅作为<strong>建议参考</strong>,不会自动应用到线上内容</li>
|
||||
<li>所有 AI 输出需要<strong>人工审核确认</strong>后才能发布</li>
|
||||
<li>AI 默认关闭,需管理员显式启用并配置 API Key</li>
|
||||
<li>API Key 不会回显到前端,不会通过 <code>wp_localize_script</code> 透出</li>
|
||||
<li>内置速率限制,防止滥用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import PageHeader from '@/components/PageHeader.vue';
|
||||
|
||||
const API = '/wp-json/aksrt/v1/ai';
|
||||
|
||||
export default {
|
||||
name: 'ExtensionAI',
|
||||
components: { PageHeader },
|
||||
setup() {
|
||||
const status = ref({
|
||||
enabled: false,
|
||||
provider: 'none',
|
||||
configured: false,
|
||||
model: '',
|
||||
api_key: '',
|
||||
rate_limit: 10,
|
||||
available: false,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
enabled: false,
|
||||
provider: 'none',
|
||||
api_key: '',
|
||||
api_base: 'https://api.openai.com/v1',
|
||||
model: 'gpt-3.5-turbo',
|
||||
max_tokens: 1000,
|
||||
rate_limit: 10,
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
const features = [
|
||||
{ key: 'seo', name: 'SEO 建议', desc: '生成标题、描述、关键词建议', scene: '文章编辑' },
|
||||
{ key: 'summary', name: '摘要生成', desc: '自动生成文章摘要', scene: '文章编辑、投稿' },
|
||||
{ key: 'tags', name: '标签建议', desc: '根据内容推荐标签', scene: '文章编辑、投稿' },
|
||||
{ key: 'excerpt', name: '摘要建议', desc: '生成吸引人的列表摘要', scene: '文章编辑' },
|
||||
];
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const res = await fetch(`${API}/status`, {
|
||||
headers: { 'X-WP-Nonce': window.aksrtAdmin?.nonce || '' },
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
status.value = json.data;
|
||||
form.enabled = json.data.enabled || false;
|
||||
form.provider = json.data.provider || 'none';
|
||||
form.model = json.data.model || 'gpt-3.5-turbo';
|
||||
form.rate_limit = json.data.rate_limit || 10;
|
||||
// 不回显 api_key
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load AI status failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = { ...form };
|
||||
// 如果密钥为空且已配置,发送掩码
|
||||
if (!payload.api_key && status.value.configured) {
|
||||
payload.api_key = '***';
|
||||
}
|
||||
const res = await fetch(`${API}/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': window.aksrtAdmin?.nonce || '',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
alert('设置已保存');
|
||||
loadStatus();
|
||||
} else {
|
||||
alert(json.message || '保存失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Save failed:', e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadStatus());
|
||||
|
||||
return { status, form, saving, features, saveSettings };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-ai__status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.aksrt-ai__status-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
.aksrt-ai__status-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.aksrt-ai__status-icon--ok {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.aksrt-ai__status-icon--warn {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.aksrt-ai__status-icon--off {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.aksrt-ai__status-icon--info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
.aksrt-ai__status-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.aksrt-ai__status-label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.aksrt-ai__form-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.aksrt-ai__form-card h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.aksrt-form-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.aksrt-ai__notice {
|
||||
background: #fff8e1;
|
||||
border-color: #ffe082;
|
||||
}
|
||||
.aksrt-ai__notice h3 {
|
||||
color: #e65100;
|
||||
}
|
||||
.aksrt-ai__notice ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.aksrt-ai__notice li {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.aksrt-ai__notice code {
|
||||
background: rgba(0,0,0,0.06);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="文档模式" desc="启用文档专题内容模式,配置文档导航页面和文档站式体验。" />
|
||||
|
||||
<n-card class="aksrt-docs-mode__card" title="文档模式" size="small">
|
||||
<n-form label-placement="left" label-width="160">
|
||||
<n-form-item label="启用文档模式">
|
||||
<n-switch v-model:value="formData.docs_mode_enabled" />
|
||||
<n-text depth="3" style="margin-left: 12px; font-size: 12px;">
|
||||
开启后将激活文档导航页面模板和文档站式侧边栏
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="文档首页模板">
|
||||
<n-select
|
||||
v-model:value="formData.docs_mode_default_template"
|
||||
:options="templateOptions"
|
||||
placeholder="选择文档首页模板"
|
||||
style="max-width: 360px;"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="文档分类">
|
||||
<n-select
|
||||
v-model:value="formData.docs_mode_cats"
|
||||
:options="categoryOptions"
|
||||
:loading="categoriesLoading"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
placeholder="选择用于文档模式的分类"
|
||||
style="max-width: 520px;"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="首页排除">
|
||||
<n-switch v-model:value="formData.docs_mode_exclude" />
|
||||
<n-text depth="3" style="margin-left: 12px; font-size: 12px;">
|
||||
开启后,所选文档分类的内容不会出现在首页常规文章流中
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="显示目录树">
|
||||
<n-switch v-model:value="formData.docs_mode_toc_enabled" />
|
||||
<n-text depth="3" style="margin-left: 12px; font-size: 12px;">
|
||||
在文档页面侧边栏显示章节目录树
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="显示章节导航">
|
||||
<n-switch v-model:value="formData.docs_mode_chapter_nav_enabled" />
|
||||
<n-text depth="3" style="margin-left: 12px; font-size: 12px;">
|
||||
在文档底部显示上一章/下一章导航
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="formData.docs_mode_enabled" label="默认排序">
|
||||
<n-radio-group v-model:value="formData.docs_mode_default_order">
|
||||
<n-radio value="menu_order">按菜单顺序</n-radio>
|
||||
<n-radio value="date">按发布时间</n-radio>
|
||||
<n-radio value="title">按标题排序</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button @click="handleReset">重置</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存设置</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="文档页面统计" size="small" class="aksrt-docs-mode__card">
|
||||
<n-grid :cols="3" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item>
|
||||
<div class="aksrt-docs-mode__stat">
|
||||
<strong>{{ docStats.total || 0 }}</strong>
|
||||
<p>文档页面总数</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="aksrt-docs-mode__stat">
|
||||
<strong>{{ docStats.nav_count || 0 }}</strong>
|
||||
<p>文档导航页</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="aksrt-docs-mode__stat">
|
||||
<strong>{{ docStats.special_count || 0 }}</strong>
|
||||
<p>专题聚合页</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card title="可用页面模板" size="small" class="aksrt-docs-mode__card">
|
||||
<n-alert type="info" size="small" style="margin-bottom: 12px;">
|
||||
以下页面模板可用于创建文档类页面。在 WordPress 后台新建页面时,从"页面属性 → 模板"中选择对应模板。
|
||||
</n-alert>
|
||||
<n-list bordered size="small">
|
||||
<n-list-item v-for="tpl in availableTemplates" :key="tpl.file">
|
||||
<n-thing :title="tpl.name" :description="tpl.description">
|
||||
<template #avatar>
|
||||
<FaIcon :icon="tpl.icon" :size="18" color="#1890ff" />
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-tag size="small" type="info">{{ tpl.file }}</n-tag>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
|
||||
<n-card title="快速入口" size="small" class="aksrt-docs-mode__card">
|
||||
<n-space>
|
||||
<router-link to="/page-display/special" class="aksrt-docs-mode__link">
|
||||
<FaIcon icon="fas fa-layer-group" :size="14" />
|
||||
专题页面配置
|
||||
</router-link>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
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 message = useMessage()
|
||||
const saving = ref(false)
|
||||
const docStats = ref({ total: 0, nav_count: 0, special_count: 0 })
|
||||
const categoriesLoading = ref(false)
|
||||
const categoryOptions = ref([])
|
||||
|
||||
const defaults = {
|
||||
docs_mode_enabled: false,
|
||||
docs_mode_default_template: 'documentnav',
|
||||
docs_mode_toc_enabled: true,
|
||||
docs_mode_chapter_nav_enabled: true,
|
||||
docs_mode_default_order: 'menu_order',
|
||||
docs_mode_cats: [],
|
||||
docs_mode_exclude: true,
|
||||
}
|
||||
|
||||
const formData = ref({ ...defaults })
|
||||
|
||||
const templateOptions = [
|
||||
{ label: '文档导航页 (page-documentnav)', value: 'documentnav' },
|
||||
{ label: '专题聚合页 (page-special)', value: 'special' },
|
||||
]
|
||||
|
||||
const availableTemplates = [
|
||||
{
|
||||
file: 'page-documentnav.php',
|
||||
name: '文档导航页',
|
||||
description: '站式文档导航模板,支持侧边栏目录树、章节高亮和快速搜索。',
|
||||
icon: 'fas fa-book',
|
||||
},
|
||||
{
|
||||
file: 'page-special.php',
|
||||
name: '专题聚合页',
|
||||
description: '专题内容聚合模板,支持封面、摘要、多列布局和子页面矩阵。',
|
||||
icon: 'fas fa-layer-group',
|
||||
},
|
||||
{
|
||||
file: 'page-download.php',
|
||||
name: '下载中心页',
|
||||
description: '资源下载中心模板,支持资源类型筛选、权限说明和交易承接。',
|
||||
icon: 'fas fa-download',
|
||||
},
|
||||
{
|
||||
file: 'page-categories.php',
|
||||
name: '分类导航页',
|
||||
description: '全站分类导航模板,展示分类树和文章统计。',
|
||||
icon: 'fas fa-folder-open',
|
||||
},
|
||||
{
|
||||
file: 'page-friends.php',
|
||||
name: '友情链接页',
|
||||
description: '友情链接展示模板。',
|
||||
icon: 'fas fa-link',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadSettings(), loadDocStats(), loadCategories()])
|
||||
})
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await api.getSchemaPage('extensions', 'docs-mode')
|
||||
if (res?.data) {
|
||||
formData.value = {
|
||||
...defaults,
|
||||
...res.data,
|
||||
docs_mode_cats: normalizeCategoryIds(res.data.docs_mode_cats),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Schema endpoint may not exist yet; use defaults
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocStats() {
|
||||
try {
|
||||
const statsRes = await api.request('/docs/stats')
|
||||
if (statsRes?.data) {
|
||||
docStats.value = statsRes.data
|
||||
}
|
||||
} catch {
|
||||
// Endpoint may not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
categoriesLoading.value = true
|
||||
try {
|
||||
const res = await api.getCategories()
|
||||
const items = res?.data || res || []
|
||||
categoryOptions.value = (Array.isArray(items) ? items : []).map((item) => ({
|
||||
label: item.name + (item.count ? ` (${item.count})` : ''),
|
||||
value: Number(item.term_id),
|
||||
}))
|
||||
} catch {
|
||||
categoryOptions.value = []
|
||||
} finally {
|
||||
categoriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCategoryIds(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => Number(item)).filter((item) => Number.isFinite(item) && item > 0)
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => Number(item.trim()))
|
||||
.filter((item) => Number.isFinite(item) && item > 0)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.saveSchemaPage('extensions', 'docs-mode', {
|
||||
...formData.value,
|
||||
docs_mode_cats: normalizeCategoryIds(formData.value.docs_mode_cats),
|
||||
})
|
||||
message.success('文档模式设置已保存')
|
||||
} catch (err) {
|
||||
message.error('保存失败: ' + (err?.message || '未知错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formData.value = { ...defaults }
|
||||
message.info('已重置为默认值')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-docs-mode__card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-docs-mode__stat {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.aksrt-docs-mode__stat strong {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-docs-mode__stat p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.aksrt-docs-mode__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
background: #f5f7fa;
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.aksrt-docs-mode__link:hover {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<ExtensionsSectionPage
|
||||
title="网站安全"
|
||||
desc="扩展安全策略和第三方安全集成。"
|
||||
>
|
||||
<n-card title="安全概览" size="small" class="aksrt-security__section">
|
||||
<n-grid :cols="4" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item v-for="item in securityItems" :key="item.key">
|
||||
<div class="aksrt-security__overview-card">
|
||||
<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>
|
||||
|
||||
<n-card title="安全能力" size="small" class="aksrt-security__section">
|
||||
<n-list bordered size="small">
|
||||
<n-list-item v-for="feat in features" :key="feat.key">
|
||||
<n-thing :title="feat.name" :description="feat.description">
|
||||
<template #avatar>
|
||||
<FaIcon :icon="feat.icon" :size="16" :color="feat.color" />
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-space align="center" size="small">
|
||||
<n-tag :type="feat.enabled ? 'success' : 'default'" size="small">
|
||||
{{ feat.enabled ? '已启用' : '未启用' }}
|
||||
</n-tag>
|
||||
<router-link v-if="feat.settingsRoute" :to="feat.settingsRoute" class="aksrt-security__config-link">
|
||||
前往配置
|
||||
</router-link>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
|
||||
<n-card title="在线测试" size="small" class="aksrt-security__section">
|
||||
<n-grid :cols="2" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="IP 归属地测试">
|
||||
<n-input v-model:value="ipTest.ip" placeholder="输入 IPv4 / IPv6,例如 8.8.8.8" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<n-button :loading="ipTest.loading" @click="handleTestIp">测试 IP 接口</n-button>
|
||||
</n-space>
|
||||
<pre v-if="ipTest.result" class="aksrt-security__output">{{ ipTest.result }}</pre>
|
||||
</n-form>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="内容审核测试">
|
||||
<n-input v-model:value="auditTest.text" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" placeholder="输入一段测试文本,检查当前审核策略是否生效" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<n-button :loading="auditTest.loading" @click="handleTestAudit">测试内容审核</n-button>
|
||||
</n-space>
|
||||
<pre v-if="auditTest.result" class="aksrt-security__output">{{ auditTest.result }}</pre>
|
||||
</n-form>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card title="风险开关" size="small" class="aksrt-security__section">
|
||||
<n-alert type="warning" size="small" style="margin-bottom: 12px;">
|
||||
以下开关会影响站点安全策略,请谨慎操作。
|
||||
</n-alert>
|
||||
<n-form label-placement="left" label-width="200" size="small">
|
||||
<n-form-item label="允许未审核评论显示">
|
||||
<n-switch v-model:value="riskSwitches.show_unapproved_comments" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后未审核评论也会在前台显示(标记为待审核)
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许前台投稿无需审核">
|
||||
<n-switch v-model:value="riskSwitches.skip_submit_moderation" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后用户投稿直接发布,不经过审核流程
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许游客评论">
|
||||
<n-switch v-model:value="riskSwitches.guest_comment" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后未登录用户也可以发表评论
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" size="small" :loading="saving" @click="handleSaveRisk">保存风险设置</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="安全审计" size="small" class="aksrt-security__section">
|
||||
<n-empty v-if="auditLogs.length === 0" description="暂无审计记录" />
|
||||
<n-list v-else bordered size="small">
|
||||
<n-list-item v-for="log in auditLogs" :key="log.id">
|
||||
<n-thing :title="log.action" :description="log.detail">
|
||||
<template #header-extra>
|
||||
<n-text depth="3" style="font-size: 11px;">{{ log.time }}</n-text>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
</ExtensionsSectionPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import ExtensionsSectionPage from '@/pages/extensions/ExtensionsSectionPage.vue'
|
||||
import FaIcon from '@/components/form/FaIcon.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const message = useMessage()
|
||||
const saving = ref(false)
|
||||
|
||||
const securityItems = ref([
|
||||
{ key: 'email_gate', icon: 'fas fa-envelope', color: '#1890ff', value: '—', label: '邮箱验证' },
|
||||
{ key: 'captcha', icon: 'fas fa-shield-alt', color: '#52c41a', value: '—', label: '验证码保护' },
|
||||
{ key: 'moderation', icon: 'fas fa-gavel', color: '#722ed1', value: '—', label: '内容审核' },
|
||||
{ key: 'ip_geo', icon: 'fas fa-map-marker-alt', color: '#fa8c16', value: '—', label: 'IP 归属地' },
|
||||
])
|
||||
|
||||
const features = ref([
|
||||
{ key: 'email_verification', icon: 'fas fa-envelope', color: '#1890ff', name: '邮箱验证', description: '新用户注册需要邮箱验证', enabled: false, settingsRoute: '/users/auth' },
|
||||
{ key: 'captcha_comment', icon: 'fas fa-shield-alt', color: '#52c41a', name: '评论验证码', description: '发表评论时需要人机验证', enabled: false, settingsRoute: '/permissions/login' },
|
||||
{ key: 'captcha_download', icon: 'fas fa-shield-alt', color: '#52c41a', name: '下载验证码', description: '下载资源时需要人机验证', enabled: false, settingsRoute: '/permissions/login' },
|
||||
{ key: 'content_audit', icon: 'fas fa-shield-virus', color: '#722ed1', name: '内容审核 API', description: '接入第三方内容安全审核', enabled: false, settingsRoute: '/extensions/audit' },
|
||||
{ key: 'ip_geolocation', icon: 'fas fa-map-marker-alt', color: '#fa8c16', name: 'IP 归属地', description: '在评论和用户主页显示 IP 归属地', enabled: false, settingsRoute: '/extensions/ip' },
|
||||
])
|
||||
|
||||
const riskSwitches = ref({
|
||||
show_unapproved_comments: false,
|
||||
skip_submit_moderation: false,
|
||||
guest_comment: false,
|
||||
})
|
||||
|
||||
const auditLogs = ref([])
|
||||
const ipTest = ref({ ip: '8.8.8.8', loading: false, result: '' })
|
||||
const auditTest = ref({ text: '这是一段用于审核测试的示例文本。', loading: false, result: '' })
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.request('/settings/security')
|
||||
if (res?.data) {
|
||||
const d = res.data
|
||||
const svc = res.services || {}
|
||||
|
||||
securityItems.value[0].value = d.email_gate_enabled ? '已开启' : '未开启'
|
||||
securityItems.value[0].color = d.email_gate_enabled ? '#52c41a' : '#8a909c'
|
||||
securityItems.value[1].value = d.captcha_enabled ? '已开启' : '未开启'
|
||||
securityItems.value[1].color = d.captcha_enabled ? '#52c41a' : '#8a909c'
|
||||
|
||||
if (svc.audit?.enabled) {
|
||||
securityItems.value[2].value = svc.audit.configured ? '已配置' : '待补全'
|
||||
securityItems.value[2].color = svc.audit.configured ? '#52c41a' : '#faad14'
|
||||
} else {
|
||||
securityItems.value[2].value = '未开启'
|
||||
securityItems.value[2].color = '#8a909c'
|
||||
}
|
||||
|
||||
if (svc.ip_geo?.enabled) {
|
||||
securityItems.value[3].value = svc.ip_geo.configured ? '已配置' : '待补全'
|
||||
securityItems.value[3].color = svc.ip_geo.configured ? '#52c41a' : '#faad14'
|
||||
} else {
|
||||
securityItems.value[3].value = '未开启'
|
||||
securityItems.value[3].color = '#8a909c'
|
||||
}
|
||||
|
||||
features.value[0].enabled = !!d.email_gate_enabled
|
||||
features.value[1].enabled = !!d.captcha_comment_enabled
|
||||
features.value[2].enabled = !!d.captcha_download_enabled
|
||||
features.value[3].enabled = !!(svc.audit?.enabled ?? d.content_audit_enabled)
|
||||
features.value[4].enabled = !!(svc.ip_geo?.enabled ?? d.ip_geo_enabled)
|
||||
|
||||
if (d.risk_switches) {
|
||||
riskSwitches.value = { ...riskSwitches.value, ...d.risk_switches }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Endpoint may not exist yet
|
||||
}
|
||||
|
||||
try {
|
||||
const logRes = await api.request('/security/audit-log')
|
||||
if (logRes?.data?.logs) {
|
||||
auditLogs.value = logRes.data.logs.slice(0, 10)
|
||||
}
|
||||
} catch {
|
||||
// Endpoint may not exist yet
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSaveRisk() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.request('/settings/security', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ risk_switches: riskSwitches.value }),
|
||||
})
|
||||
message.success('风险设置已保存')
|
||||
} catch (err) {
|
||||
message.error('保存失败: ' + (err?.message || '未知错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestIp() {
|
||||
ipTest.value.loading = true
|
||||
try {
|
||||
const res = await api.testIpGeo(ipTest.value.ip)
|
||||
ipTest.value.result = JSON.stringify(res?.data || {}, null, 2)
|
||||
message.success('IP 接口测试完成')
|
||||
} catch (err) {
|
||||
message.error(err?.message || 'IP 测试失败')
|
||||
} finally {
|
||||
ipTest.value.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestAudit() {
|
||||
auditTest.value.loading = true
|
||||
try {
|
||||
const res = await api.testContentAudit(auditTest.value.text)
|
||||
auditTest.value.result = JSON.stringify(res?.data || {}, null, 2)
|
||||
message.success('内容审核测试完成')
|
||||
} catch (err) {
|
||||
message.error(err?.message || '内容审核测试失败')
|
||||
} finally {
|
||||
auditTest.value.loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-security__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.aksrt-security__config-link {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-security__config-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.aksrt-security__output {
|
||||
margin: 12px 0 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #101828;
|
||||
color: #d0d7e2;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,271 @@
|
||||
<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="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="fas fa-broom" :size="12" /></template>
|
||||
清除缓存
|
||||
</n-button>
|
||||
<n-button @click="handleOptimizeDb">
|
||||
<template #icon><FaIcon icon="fas fa-database" :size="12" /></template>
|
||||
优化数据表
|
||||
</n-button>
|
||||
<n-button @click="handleRebuildIndex">
|
||||
<template #icon><FaIcon icon="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: 'fas fa-code', color: '#1890ff', value: '—', label: 'PHP 版本', status: 'ok' },
|
||||
{ key: 'wp', icon: 'fab fa-wordpress', color: '#722ed1', value: '—', label: 'WordPress', status: 'ok' },
|
||||
{ key: 'db', icon: 'fas fa-database', color: '#52c41a', value: '—', label: '数据库', status: 'ok' },
|
||||
{ key: 'uploads', icon: 'fas fa-folder-open', color: '#fa8c16', value: '—', label: '上传目录', status: 'ok' },
|
||||
])
|
||||
|
||||
const tools = ref([
|
||||
{ key: 'import-export', icon: 'fas fa-exchange-alt', bg: '#1890ff', title: '导入导出', desc: '设置导入、导出和重置', route: '/tools/import-export' },
|
||||
{ key: 'health', icon: 'fas fa-heartbeat', bg: '#52c41a', title: '健康检查', desc: '系统状态检测和诊断', route: '/tools/health' },
|
||||
{ key: 'migration', icon: 'fas fa-exchange-alt', bg: '#722ed1', title: '数据迁移', desc: '设置迁移状态和兼容性', route: '/tools/migration' },
|
||||
{ key: 'email-rollback', icon: 'fas fa-undo', bg: '#fa8c16', title: '邮箱回滚', desc: '邮箱变更 14 天内回滚', route: '/tools/email-rollback' },
|
||||
])
|
||||
|
||||
const services = ref([
|
||||
{ key: 'sms', icon: 'fas fa-sms', color: '#1890ff', name: '短信服务', description: '验证码和通知短信', available: false },
|
||||
{ key: 'wechat', icon: 'fab fa-weixin', color: '#52c41a', name: '微信通知', description: '微信模板消息和订阅通知', available: false },
|
||||
{ key: 'captcha', icon: 'fas fa-shield-alt', color: '#722ed1', name: '验证码服务', description: '人机验证(极验/腾讯/阿里云)', available: false },
|
||||
{ key: 'push', icon: 'fas fa-paper-plane', color: '#fa8c16', name: '搜索引擎推送', description: '百度/Google 主动推送', available: false },
|
||||
{ key: 'audit', icon: 'fas fa-shield-virus', 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 ?? s.captcha_enabled)
|
||||
services.value[2].status = svc.captcha?.provider || (s.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: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-tools__health-card p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.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: #1890ff;
|
||||
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: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-tools__tool-body p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader :title="title" :desc="desc">
|
||||
<template #actions>
|
||||
<n-space v-if="links.length">
|
||||
<n-button v-for="link in links" :key="link.path" quaternary @click="go(link.path)">
|
||||
{{ link.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-card v-if="note" class="aksrt-section-card">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
{{ note }}
|
||||
</n-alert>
|
||||
</n-card>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
desc: { type: String, default: '' },
|
||||
note: { type: String, default: '' },
|
||||
links: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user