Add pagination to admin management tabs (cases/categories/sources/orgs)

This commit is contained in:
Yuzhiran Dev
2026-05-18 19:18:09 +08:00
parent 7169ddb678
commit a09e7e6671
14 changed files with 455 additions and 340 deletions
+202 -11
View File
@@ -26,9 +26,11 @@
<el-button size="default" :type="activeTab === 'tasklogs' ? 'primary' : ''" @click="switchTab('tasklogs')">任务日志</el-button>
<el-button size="default" :type="activeTab === 'llmconfigs' ? 'primary' : ''" @click="switchTab('llmconfigs')">LLM配置</el-button>
<el-button size="default" :type="activeTab === 'systemconfigs' ? 'primary' : ''" @click="switchTab('systemconfigs')">系统配置</el-button>
<el-button size="default" :type="activeTab === 'users' ? 'primary' : ''" @click="switchTab('users')">用户管理</el-button>
<el-button size="default" :type="activeTab === 'categories' ? 'primary' : ''" @click="switchTab('categories')">采集类别</el-button>
<el-button size="default" :type="activeTab === 'sources' ? 'primary' : ''" @click="switchTab('sources')">信息源</el-button>
<el-button size="default" :type="activeTab === 'orgs' ? 'primary' : ''" @click="switchTab('orgs')">组织管理</el-button>
<el-button size="default" :type="activeTab === 'logs' ? 'primary' : ''" @click="switchTab('logs')">运行日志</el-button>
</div>
</div>
@@ -48,7 +50,7 @@
<div class="stat-summary">
<div class="stat-item"><span class="num">{{ cases.length }}</span><span class="label">总案例</span></div>
</div>
<el-table :data="cases" border stripe class="data-table" style="width:100%">
<el-table :data="paginatedCases" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="title" label="标题" min-width="120" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
@@ -61,9 +63,12 @@
</template>
</el-table-column>
</el-table>
<div v-if="cases.length > casePageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="cases.length" :page-size="casePageSize" :current-page="casePage" @current-change="casePage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile">
<div v-for="item in cases" :key="item.id" class="card-item">
<div v-for="item in paginatedCases" :key="item.id" class="card-item">
<div class="card-row"><span class="card-label">标题</span><span class="card-value">{{ item.title }}</span></div>
<div class="card-row"><span class="card-label">领域</span><span class="card-value">{{ item.field }}</span></div>
<div class="card-row"><span class="card-label">来源</span><span class="card-value">{{ item.source }}</span></div>
@@ -198,6 +203,71 @@
</div>
</div>
<div v-if="activeTab === 'users'">
<div class="toolbar">
<el-button type="primary" size="small" @click="addUser">+ 新建用户</el-button>
<el-button size="small" @click="fetchUsers">刷新</el-button>
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ users.length }} 个</span>
</div>
<div v-if="usersLoading" class="card-loading">加载中...</div>
<template v-else-if="users.length === 0">
<div class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconUser /></el-icon>
<div class="empty-text">暂无用户</div>
<el-button type="primary" size="small" @click="addUser">新增第一个用户</el-button>
</div>
</template>
<template v-else>
<el-table :data="paginatedUsers" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="role" label="角色" width="100">
<template #default="scope"><el-tag :type="scope.row.role === 'admin' ? 'danger' : 'info'">{{ scope.row.role === 'admin' ? '管理员' : '编辑' }}</el-tag></template>
</el-table-column>
<el-table-column prop="org_id" label="组织" width="100"></el-table-column>
<el-table-column prop="created_at" label="创建时间"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button size="small" type="danger" @click="deleteUser(scope.row.id)" :disabled="scope.row.role === 'admin'">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="users.length > userPageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="users.length" :page-size="userPageSize" :current-page="userPage" @current-change="userPage = $event"></el-pagination>
</div>
<div class="card-list-mobile">
<div v-for="u in users" :key="u.id" class="card-item">
<div class="card-row"><span class="card-label">用户名</span><span class="card-value">{{ u.username }} <el-tag size="small" :type="u.role === 'admin' ? 'danger' : 'info'">{{ u.role === 'admin' ? '管理员' : '编辑' }}</el-tag></span></div>
<div class="card-row"><span class="card-label">组织</span><span class="card-value">{{ u.org_id || '-' }}</span></div>
<div class="card-row"><span class="card-label">创建时间</span><span class="card-value">{{ formatDate(u.created_at) }}</span></div>
<div class="card-actions">
<el-button size="small" type="danger" plain @click="deleteUser(u.id)" :disabled="u.role === 'admin'">删除</el-button>
</div>
</div>
</div>
</template>
<el-dialog v-model="userDialogVisible" title="新建用户" width="400px" :close-on-click-modal="false">
<el-form :model="userForm" label-width="80px">
<el-form-item label="用户名" required>
<el-input v-model="userForm.username" placeholder="2-20个字符" maxlength="20" clearable></el-input>
</el-form-item>
<el-form-item label="密码" required>
<el-input v-model="userForm.password" type="password" placeholder="至少6位" show-password></el-input>
</el-form-item>
<el-form-item label="角色">
<el-select v-model="userForm.role" style="width:100%">
<el-option label="编辑" value="editor"></el-option>
<el-option label="管理员" value="admin"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUser" :loading="userSubmitting">创建</el-button>
</template>
</el-dialog>
</div>
<div v-if="activeTab === 'categories'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showCategoryDialog()">新增类别</el-button>
@@ -212,7 +282,7 @@
</div>
</template>
<template v-else>
<el-table :data="categories" border stripe class="data-table" style="width:100%">
<el-table :data="paginatedCategories" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="name" label="名称" min-width="120"></el-table-column>
<el-table-column prop="search_query" label="搜索词" min-width="200"></el-table-column>
@@ -230,9 +300,12 @@
</template>
</el-table-column>
</el-table>
<div v-if="categories.length > categoryPageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="categories.length" :page-size="categoryPageSize" :current-page="categoryPage" @current-change="categoryPage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile" v-if="categories.length > 0">
<div v-for="item in categories" :key="item.id" class="card-item">
<div v-for="item in paginatedCategories" :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.search_query }}</span></div>
<div class="card-row"><span class="card-label">源数</span><span class="card-value">{{ item.source_count }}</span></div>
@@ -259,7 +332,7 @@
</div>
</template>
<template v-else>
<el-table :data="orgs" border stripe class="data-table" style="width:100%">
<el-table :data="paginatedOrgs" border stripe class="data-table" style="width:100%">
<el-table-column prop="org_id" label="组织ID" min-width="120"></el-table-column>
<el-table-column prop="name" label="名称" min-width="150"></el-table-column>
<el-table-column prop="description" label="描述" min-width="200" :show-overflow-tooltip="true"></el-table-column>
@@ -277,9 +350,12 @@
</template>
</el-table-column>
</el-table>
<div v-if="orgs.length > orgPageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="orgs.length" :page-size="orgPageSize" :current-page="orgPage" @current-change="orgPage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile" v-if="orgs.length > 0">
<div v-for="item in orgs" :key="item.org_id" class="card-item">
<div v-for="item in paginatedOrgs" :key="item.org_id" class="card-item">
<div class="card-row"><span class="card-label">ID</span><span class="card-value">{{ item.org_id }}</span></div>
<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.user_count }}</span></div>
@@ -292,6 +368,32 @@
</div>
</div>
<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>
<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>
</div>
<div v-if="logsLoading" class="card-loading">加载中...</div>
<div v-else-if="!logContent" class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
<div class="empty-text">选择日志类型和日期后点击加载</div>
</div>
<div v-else class="log-container" style="max-height:600px;overflow-y:auto;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;">
<pre style="margin:0;padding:16px;white-space:pre-wrap;word-wrap:break-word;font-size:13px;line-height:1.6;color:#303133;">{{ logContent }}</pre>
</div>
</div>
<div v-if="activeTab === 'sources'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showSourceDialog()">新增信息源</el-button>
@@ -307,7 +409,7 @@
</div>
</template>
<template v-else>
<el-table :data="sources" border stripe class="data-table" style="width:100%">
<el-table :data="paginatedSources" 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="名称" min-width="130"></el-table-column>
<el-table-column prop="source_type" label="类型" width="90">
@@ -328,9 +430,12 @@
</template>
</el-table-column>
</el-table>
<div v-if="sources.length > sourcePageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="sources.length" :page-size="sourcePageSize" :current-page="sourcePage" @current-change="sourcePage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile" v-if="sources.length > 0">
<div v-for="item in sources" :key="item.id" class="card-item">
<div v-for="item in paginatedSources" :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.source_type }}</span></div>
<div class="card-row"><span class="card-label">查询</span><span class="card-value">{{ item.query || item.url }}</span></div>
@@ -483,7 +588,7 @@
<script src="icon-components.js"></script>
<script src="element-plus.full.js"></script>
<script>
const { createApp, ref, reactive, onMounted } = Vue;
const { createApp, ref, reactive, computed, onMounted } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
@@ -513,7 +618,7 @@
})
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; });
const redirectToPage = (page) => { window.location.href = '/' + page; };
const redirectToPage = (page) => { window.location.href = page.startsWith('/') ? page : '/' + page; };
const cases = ref([]);
const casesLoading = ref(false);
@@ -543,6 +648,9 @@
try { await ElMessageBox.confirm('确定删除该案例吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/cases/${id}`); ElMessage.success('删除成功'); await loadCases(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const casePage = ref(1);
const casePageSize = 20;
const paginatedCases = computed(() => { const s = (casePage.value - 1) * casePageSize; return cases.value.slice(s, s + casePageSize); });
const taskLogs = ref([]);
const taskLogsLoading = ref(false);
@@ -642,6 +750,72 @@
try { await ElMessageBox.confirm('确定删除该组织吗?关联的用户和选题不会被删除。', '提示', { type: 'warning' }); await api.delete(`/api/admin/orgs/${orgId}`); ElMessage.success('删除成功'); await loadOrgs(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const orgPage = ref(1);
const orgPageSize = 20;
const paginatedOrgs = computed(() => { const s = (orgPage.value - 1) * orgPageSize; return orgs.value.slice(s, s + orgPageSize); });
const users = ref([]);
const usersLoading = ref(false);
const userDialogVisible = ref(false);
const userSubmitting = ref(false);
const userForm = reactive({ username: '', password: '', role: 'editor' });
const userPage = ref(1);
const userPageSize = 20;
const paginatedUsers = computed(() => {
const start = (userPage.value - 1) * userPageSize;
return users.value.slice(start, start + userPageSize);
});
const fetchUsers = async () => {
usersLoading.value = true;
try { users.value = await api.get('/api/admin/users'); } catch (e) { ElMessage.error('加载用户失败: ' + e.message); }
finally { usersLoading.value = false; }
};
const addUser = () => { userForm.username = ''; userForm.password = ''; userForm.role = 'editor'; userDialogVisible.value = true; };
const submitUser = async () => {
if (!userForm.username || userForm.username.length < 2) { ElMessage.warning('用户名至少2个字符'); return; }
if (!userForm.password || userForm.password.length < 6) { ElMessage.warning('密码至少6位'); return; }
userSubmitting.value = true;
try {
const u = await api.post('/api/admin/users', { ...userForm });
users.value.push(u);
ElMessage.success('创建成功');
userDialogVisible.value = false;
} catch (e) { ElMessage.error('创建失败: ' + e.message); }
finally { userSubmitting.value = false; }
};
const deleteUser = async (id) => {
if (id === 'admin') { ElMessage.warning('不能删除管理员用户'); return; }
try {
await ElMessageBox.confirm('确定删除该用户?', '提示', { type: 'warning' });
await api.delete(`/api/admin/users/${encodeURIComponent(id)}`);
users.value = users.value.filter(u => u.id !== id);
ElMessage.success('删除成功');
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const logType = ref('creator');
const logDate = ref(new Date().toISOString().slice(0, 10));
const logContent = ref('');
const logsLoading = ref(false);
const fetchLogs = async () => {
logsLoading.value = true;
try {
const resp = await fetch(`/api/system/logs/${logDate.value}?log_type=${logType.value}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` }
});
if (resp.ok) {
const data = await resp.json();
const lines = data.content || [];
logContent.value = lines.join('\n');
} else if (resp.status === 404) {
logContent.value = '该日暂无日志';
} else {
logContent.value = `请求失败: ${resp.status}`;
}
} catch (e) {
logContent.value = '加载失败: ' + e.message;
} finally { logsLoading.value = false; }
};
const loadSystemConfigs = async () => {
systemConfigsLoading.value = true;
@@ -686,6 +860,9 @@
try { await ElMessageBox.confirm('确定删除该类别及其关联的信息源吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/collector/categories/${id}`); ElMessage.success('删除成功'); await loadCategories(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const categoryPage = ref(1);
const categoryPageSize = 20;
const paginatedCategories = computed(() => { const s = (categoryPage.value - 1) * categoryPageSize; return categories.value.slice(s, s + categoryPageSize); });
const loadSources = async () => {
sourcesLoading.value = true;
@@ -708,12 +885,17 @@
try { await ElMessageBox.confirm('确定删除该信息源吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/collector/sources/${id}`); ElMessage.success('删除成功'); await loadSources(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const sourcePage = ref(1);
const sourcePageSize = 20;
const paginatedSources = computed(() => { const s = (sourcePage.value - 1) * sourcePageSize; return sources.value.slice(s, s + sourcePageSize); });
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' }); };
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
const tabLoaders = {
cases: loadCases, tasklogs: loadTaskLogs,
llmconfigs: loadLLMConfigs, systemconfigs: loadSystemConfigs,
users: fetchUsers,
categories: loadCategories, sources: loadSources,
orgs: loadOrgs,
};
@@ -730,18 +912,27 @@
onMounted(() => {
loadedTabs.add('cases');
loadCases();
const hash = window.location.hash.replace('#tab=', '');
if (hash && tabLoaders[hash]) { switchTab(hash); }
});
return {
activeTab, switchTab,
cases, casesLoading, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
casePage, casePageSize, paginatedCases,
taskLogs, taskLogsLoading, loadTaskLogs,
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
categories, categoriesLoading, categoryDialogVisible, categoryForm, categoryDialogTitle, showCategoryDialog, saveCategory, deleteCategory,
categoryPage, categoryPageSize, paginatedCategories,
sources, sourcesLoading, sourceDialogVisible, sourceForm, sourceDialogTitle, showSourceDialog, saveSource, deleteSource,
sourcePage, sourcePageSize, paginatedSources,
orgs, orgsLoading, orgDialogVisible, orgForm, orgDialogTitle, showOrgDialog, saveOrg, deleteOrg,
logout, currentUser, isAdmin, redirectToPage
orgPage, orgPageSize, paginatedOrgs,
logout, currentUser, isAdmin, redirectToPage,
users, usersLoading, userDialogVisible, userSubmitting, userForm, userPage, userPageSize, paginatedUsers,
fetchUsers, addUser, submitUser, deleteUser,
logType, logDate, logContent, logsLoading, fetchLogs
};
}
});