fix: content quality, image format, task monitor, calendar data source, search UI & sort

This commit is contained in:
Yuzhiran Dev
2026-05-26 11:26:10 +08:00
parent b2d043b231
commit ac8644d752
36 changed files with 2294 additions and 873 deletions
+221 -18
View File
@@ -31,6 +31,7 @@
<el-button size="default" :type="activeTab === 'menus' ? 'primary' : ''" @click="switchTab('menus')">菜单管理</el-button>
<el-button size="default" :type="activeTab === 'logs' ? 'primary' : ''" @click="switchTab('logs')">运行日志</el-button>
<el-button size="default" :type="activeTab === 'assistant' ? 'primary' : ''" @click="switchTab('assistant')">AI 助手</el-button>
<el-button size="default" :type="activeTab === 'searchproviders' ? 'primary' : ''" @click="switchTab('searchproviders')">搜索API</el-button>
</div>
</div>
@@ -441,16 +442,8 @@
<div v-if="activeTab === 'logs'">
<div class="toolbar">
<el-select v-model="logType" placeholder="日志类型" style="width:200px;">
<el-option label="创作日志" value="creator"></el-option>
<el-option label="审查日志" value="optimizer"></el-option>
<el-option label="研究日志" value="research"></el-option>
<el-option label="大纲日志" value="outline"></el-option>
<el-option label="写作日志" value="writer"></el-option>
<el-option label="发布日志" value="publisher"></el-option>
<el-option label="收集日志" value="collector"></el-option>
<el-option label="通知日志" value="notifier"></el-option>
<el-option label="趋势日志" value="trends"></el-option>
<el-select v-model="logType" placeholder="日志类型" style="width:200px;" @focus="loadLogTypes">
<el-option v-for="t in logTypes" :key="t.module_id" :label="t.name" :value="t.module_id"></el-option>
</el-select>
<el-date-picker v-model="logDate" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width:200px;"></el-date-picker>
<el-button type="primary" @click="fetchLogs" :loading="logsLoading">加载日志</el-button>
@@ -480,7 +473,119 @@
</el-form>
</div>
<div v-if="activeTab === 'searchproviders'">
<div class="toolbar">
<el-button type="primary" size="small" @click="addSearchProvider">新增提供商</el-button>
<el-button size="small" @click="testAllSearchProviders">测试全部</el-button>
<el-button size="small" @click="resetSearchUsage">重置用量</el-button>
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ searchProviders.length }} 个</span>
</div>
<div v-if="searchProvidersLoading" class="card-loading">加载中...</div>
<template v-else-if="searchProviders.length === 0">
<div class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconSearch /></el-icon>
<div class="empty-text">暂无搜索API提供商</div>
</div>
</template>
<template v-else>
<el-table :data="searchProviders" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="50"></el-table-column>
<el-table-column prop="name" label="名称" width="120"></el-table-column>
<el-table-column prop="provider_type" label="类型" width="100">
<template #default="scope">
<el-tag :type="scope.row.provider_type === 'baidu' ? 'primary' : scope.row.provider_type === 'bing' ? 'success' : 'warning'" size="small">{{ scope.row.provider_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="enabled" label="启用" width="60">
<template #default="scope">
<el-switch v-model="scope.row.enabled" @change="updateSearchProvider(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="70"></el-table-column>
<el-table-column prop="daily_limit" label="日限" width="70"></el-table-column>
<el-table-column prop="usage_today" label="已用/日限" width="120">
<template #default="scope">
<div style="display:flex;align-items:center;gap:4px;">
<el-progress :percentage="Math.round(scope.row.usage_today / scope.row.daily_limit * 100)" :stroke-width="12" :status="scope.row.usage_today >= scope.row.daily_limit ? 'exception' : scope.row.usage_today / scope.row.daily_limit > 0.8 ? 'warning' : 'success'" style="flex:1;min-width:80px;"></el-progress>
<span :style="{fontSize:'11px',color: scope.row.usage_today >= scope.row.daily_limit ? '#f56c6c' : scope.row.usage_today / scope.row.daily_limit > 0.8 ? '#e6a23c' : '#67c23a'}">{{ scope.row.usage_today }}/{{ scope.row.daily_limit }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="api_url" label="API地址" min-width="200">
<template #default="scope">
<span style="font-size:12px;word-break:break-all;">{{ scope.row.api_url || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="console_url" label="控制台" min-width="160">
<template #default="scope">
<a v-if="scope.row.console_url" :href="scope.row.console_url" target="_blank" style="font-size:12px;color:#409eff;">打开</a>
<span v-else style="color:#c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="editSearchProvider(scope.row)">编辑</el-button>
<el-button size="small" @click="testSearchProvider(scope.row)">测试</el-button>
<el-button size="small" type="danger" @click="deleteSearchProvider(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="item in searchProviders" :key="item.id" class="card-item">
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ item.name }}</span></div>
<div class="card-row"><span class="card-label">类型</span><span class="card-value">{{ item.provider_type }}</span></div>
<div class="card-row"><span class="card-label">用量</span><span class="card-value">{{ item.usage_today }}/{{ item.daily_limit }}</span></div>
<div class="card-actions"><el-button size="small" @click="editSearchProvider(item)">编辑</el-button><el-button size="small" type="danger" @click="deleteSearchProvider(item.id)">删除</el-button></div>
</div>
</div>
</template>
</div>
<el-dialog v-model="searchProviderDialogVisible" :title="searchProviderDialogTitle" width="720px" :close-on-click-modal="false">
<el-form :model="searchProviderForm" label-width="110px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="名称"><el-input v-model="searchProviderForm.name" placeholder="如:百度千帆"/></el-form-item></el-col>
<el-col :span="12"> <el-form-item label="类型">
<el-select v-model="searchProviderForm.provider_type" style="width:100%">
<el-option label="百度千帆" value="baidu"></el-option>
<el-option label="opencode云搜索" value="mcp"></el-option>
</el-select>
</el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="API Key"><el-input v-model="searchProviderForm.api_key" type="password" show-password placeholder="sk-..."/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="优先级"><el-input-number v-model="searchProviderForm.priority" :min="1" :max="99" style="width:100%"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="14"><el-form-item label="API地址"><el-input v-model="searchProviderForm.api_url" placeholder="https://..."/></el-form-item></el-col>
<el-col :span="10"><el-form-item label="日限额"><el-input-number v-model="searchProviderForm.daily_limit" :min="1" :max="999999" style="width:100%"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="控制台地址"><el-input v-model="searchProviderForm.console_url" placeholder="https://console.xxx.com/..."/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="searchProviderDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveSearchProvider" :loading="searchProviderSaving">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="searchProviderTestVisible" title="测试结果" width="600px">
<div v-if="searchProviderTesting" style="text-align:center;padding:20px;">测试中...</div>
<div v-else>
<div :style="{padding:'8px',marginBottom:'8px',borderRadius:'4px',background:searchProviderTestResult.ok ? '#f0f9eb' : '#fef0f0',color:searchProviderTestResult.ok ? '#67c23a' : '#f56c6c'}">
{{ searchProviderTestResult.ok ? '✅ 连接成功' : '❌ 连接失败' }}
</div>
<div v-if="searchProviderTestResult.error" style="color:#f56c6c;font-size:13px;margin-bottom:8px;">{{ searchProviderTestResult.error }}</div>
<div v-if="searchProviderTestResult.results && searchProviderTestResult.results.length">
<div v-for="r in searchProviderTestResult.results" :key="r.url" style="padding:8px;border-bottom:1px solid #eee;">
<div style="font-weight:bold;">{{ r.title }}</div>
<div style="font-size:12px;color:#909399;">{{ r.url }}</div>
</div>
</div>
</div>
<template #footer><el-button @click="searchProviderTestVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</main>
</div>
@@ -761,6 +866,7 @@ const llmConfigs = ref([]);
};
const logType = ref('creator');
const logTypes = ref([]);
const users = ref([]);
const usersLoading = ref(false);
const userDialogVisible = ref(false);
@@ -810,10 +916,25 @@ const llmConfigs = ref([]);
const logDate = ref(new Date().toISOString().slice(0, 10));
const logContent = ref('');
const logsLoading = ref(false);
const loadLogTypes = async () => {
try {
logTypes.value = await api.get('/api/admin/task-logs/log-types');
logTypes.value.unshift({ module_id: 'creator', name: '创作日志', log_file: 'creator' });
logTypes.value.unshift({ module_id: 'optimizer', name: '审查日志', log_file: 'optimizer' });
logTypes.value.unshift({ module_id: 'research', name: '研究日志', log_file: 'research' });
logTypes.value.unshift({ module_id: 'outline', name: '大纲日志', log_file: 'outline' });
logTypes.value.unshift({ module_id: 'writer', name: '写作日志', log_file: 'writer' });
logTypes.value.unshift({ module_id: 'publisher', name: '发布日志', log_file: 'publisher' });
logTypes.value.unshift({ module_id: 'collector', name: '收集日志', log_file: 'collector' });
logTypes.value.unshift({ module_id: 'trends', name: '趋势日志', log_file: 'trends' });
} catch (e) { console.error('加载日志类型失败:', e); }
};
const fetchLogs = async () => {
logsLoading.value = true;
try {
const resp = await fetch(`/api/system/logs/${logDate.value}?log_type=${logType.value}`, {
const module = logTypes.value.find(t => t.name === logType.value);
const logFile = module ? module.log_file : logType.value;
const resp = await fetch(`/api/system/logs/${logDate.value}?log_type=${logFile}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` }
});
if (resp.ok) {
@@ -863,12 +984,89 @@ const llmConfigs = ref([]);
} catch (e) { console.error('加载AI助手配置失败', e); }
};
const saveAssistantPrompt = async () => {
assistantSaving.value = true;
try {
await api.post('/api/admin/systemconfigs', { key: 'assistant_system_prompt', value: assistantPrompt.value, description: 'AI 助手系统提示词' });
ElementPlus.ElMessage.success('保存成功');
} catch (e) { ElementPlus.ElMessage.error('保存失败: ' + e.message); }
finally { assistantSaving.value = false; }
const configs = await api.get('/api/admin/systemconfigs');
const cfg = configs.find(c => c.key === 'assistant_system_prompt');
if (cfg) { await api.put(`/api/admin/systemconfigs/${cfg.id}`, { value: assistantPrompt.value, description: 'AI 助手系统提示词' }); }
else { await api.post('/api/admin/systemconfigs', { key: 'assistant_system_prompt', value: assistantPrompt.value, description: 'AI 助手系统提示词' }); }
ElMessage.success('保存成功');
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
};
const searchProviders = ref([]);
const searchProvidersLoading = ref(false);
const searchProviderDialogVisible = ref(false);
const searchProviderDialogTitle = ref('新增搜索API');
const searchProviderSaving = ref(false);
const searchProviderForm = reactive({ id: null, name: '', provider_type: 'baidu', api_key: '', api_url: '', console_url: '', priority: 1, daily_limit: 1500, enabled: true });
const searchProviderTestVisible = ref(false);
const searchProviderTesting = ref(false);
const searchProviderTestResult = ref({ ok: false, error: '', results: [] });
const loadSearchProviders = async () => {
searchProvidersLoading.value = true;
try { searchProviders.value = await api.get('/api/search-providers'); }
catch (e) { ElMessage.error('加载搜索API失败: ' + e.message); }
finally { searchProvidersLoading.value = false; }
};
const addSearchProvider = () => {
searchProviderDialogTitle.value = '新增搜索API';
searchProviderForm.id = null;
searchProviderForm.name = '';
searchProviderForm.provider_type = 'baidu';
searchProviderForm.api_key = '';
searchProviderForm.api_url = '';
searchProviderForm.console_url = '';
searchProviderForm.priority = 1;
searchProviderForm.daily_limit = 1500;
searchProviderForm.enabled = true;
searchProviderDialogVisible.value = true;
};
const editSearchProvider = (row) => {
searchProviderDialogTitle.value = '编辑搜索API';
Object.assign(searchProviderForm, { id: row.id, name: row.name, provider_type: row.provider_type, api_key: row.api_key, api_url: row.api_url, console_url: row.console_url || '', priority: row.priority, daily_limit: row.daily_limit, enabled: row.enabled });
searchProviderDialogVisible.value = true;
};
const saveSearchProvider = async () => {
searchProviderSaving.value = true;
try {
const body = { name: searchProviderForm.name, provider_type: searchProviderForm.provider_type, api_key: searchProviderForm.api_key, api_url: searchProviderForm.api_url, console_url: searchProviderForm.console_url, priority: searchProviderForm.priority, daily_limit: searchProviderForm.daily_limit, enabled: searchProviderForm.enabled };
if (searchProviderForm.id) { await api.put(`/api/search-providers/${searchProviderForm.id}`, body); ElMessage.success('更新成功'); }
else { await api.post('/api/search-providers', body); ElMessage.success('创建成功'); }
searchProviderDialogVisible.value = false;
await loadSearchProviders();
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
finally { searchProviderSaving.value = false; }
};
const deleteSearchProvider = async (id) => {
try { await ElMessageBox.confirm('确定删除该提供商吗?', '提示', { type: 'warning' }); await api.delete(`/api/search-providers/${id}`); ElMessage.success('删除成功'); await loadSearchProviders(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const updateSearchProvider = async (row) => {
try { await api.put(`/api/search-providers/${row.id}`, { enabled: row.enabled }); }
catch (e) { ElMessage.error('更新失败: ' + e.message); }
};
const testSearchProvider = async (row) => {
searchProviderTesting.value = true;
searchProviderTestVisible.value = true;
searchProviderTestResult.value = { ok: false, error: '', results: [] };
try {
const r = await api.post(`/api/search-providers/${row.id}/test`, { query: '测试搜索' });
searchProviderTestResult.value = r;
} catch (e) { searchProviderTestResult.value = { ok: false, error: e.message, results: [] }; }
finally { searchProviderTesting.value = false; }
};
const testAllSearchProviders = async () => {
for (const p of searchProviders.value) {
try {
const r = await api.post(`/api/search-providers/${p.id}/test`, { query: '测试搜索' });
ElMessage({ type: r.ok ? 'success' : 'error', message: `${p.name}: ${r.ok ? 'OK' : '失败 ' + (r.error || '')}` });
} catch (e) { ElMessage.error(`${p.name} 测试失败: ${e.message}`); }
}
};
const resetSearchUsage = async () => {
try { await api.post('/api/search-providers/reset-usage'); ElMessage.success('用量已重置'); await loadSearchProviders(); }
catch (e) { ElMessage.error('重置失败: ' + e.message); }
};
const formatDate = (dateStr) => { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); };
@@ -877,7 +1075,8 @@ const llmConfigs = ref([]);
const tabLoaders = {
llmconfigs: loadLLMConfigs, platformconfigs: loadPlatformConfigs, systemconfigs: loadSystemConfigs,
users: fetchUsers,
orgs: loadOrgs, roles: loadRoles, menus: loadMenus, assistant: loadAssistantConfig, logs: fetchLogs,
logs: loadLogTypes, orgs: loadOrgs, roles: loadRoles, menus: loadMenus, assistant: loadAssistantConfig,
searchproviders: loadSearchProviders,
};
const loadedTabs = new Set([]);
@@ -909,8 +1108,12 @@ const llmConfigs = ref([]);
logout, currentUser, isAdmin, redirectToPage,
users, usersLoading, userDialogVisible, userDialogTitle, userSubmitting, userForm, userPage, userPageSize, paginatedUsers,
fetchUsers, addUser, showEditUserDialog, submitUser, deleteUser,
logType, logDate, logContent, logsLoading, fetchLogs,
logType, logTypes, logDate, logContent, logsLoading, fetchLogs,
assistantPrompt, assistantEnabled, assistantSaving, saveAssistantPrompt,
searchProviders, searchProvidersLoading, searchProviderDialogVisible, searchProviderDialogTitle, searchProviderSaving, searchProviderForm,
searchProviderTestVisible, searchProviderTesting, searchProviderTestResult,
loadSearchProviders, addSearchProvider, editSearchProvider, saveSearchProvider, deleteSearchProvider, updateSearchProvider,
testSearchProvider, testAllSearchProviders, resetSearchUsage,
};
}
});