init
This commit is contained in:
@@ -2,20 +2,19 @@
|
||||
<div>
|
||||
<header class="header-wrap" :class="{ 'is-scrolled': isScrolled }">
|
||||
<div class="container header">
|
||||
<a class="brand" href="#" aria-label="ToolsShow 首页" @click.prevent>
|
||||
<a class="brand" href="#" aria-label="Tools工具" @click.prevent>
|
||||
<span class="brand-mark">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M4 6.5C4 5.67 4.67 5 5.5 5H18.5C19.33 5 20 5.67 20 6.5V17.5C20 18.33 19.33 19 18.5 19H5.5C4.67 19 4 18.33 4 17.5V6.5Z" stroke="currentColor" stroke-width="1.8" />
|
||||
<path d="M8 9H16M8 12H16M8 15H13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>ToolsShow</span>
|
||||
<span>资源导航</span>
|
||||
</a>
|
||||
<nav class="nav" aria-label="主导航">
|
||||
<a href="#tools">工具列表</a>
|
||||
<a href="#tools">分类浏览</a>
|
||||
<a href="#tools">工具中心</a>
|
||||
<a href="/admin">管理端</a>
|
||||
<button
|
||||
type="button"
|
||||
class="nav-btn"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
|
||||
<main class="dashboard-main" :class="{ 'with-kpi': activeMenu === 'overview' }">
|
||||
<main class="dashboard-main" :class="{ 'with-kpi': isOverviewRoute }">
|
||||
<AdminTopbar
|
||||
v-model:search="topSearch"
|
||||
:section-title="sectionTitle"
|
||||
@@ -73,63 +73,13 @@
|
||||
@refresh="refreshCurrentSection"
|
||||
@open-overview="openOverviewSection"
|
||||
@open-tools="openToolsSection"
|
||||
@open-categories="openCategoriesSection"
|
||||
@open-audit="openAuditSection"
|
||||
/>
|
||||
|
||||
<AdminKpiRow
|
||||
v-if="activeMenu === 'overview'"
|
||||
:kpi-cards="kpiCards"
|
||||
:format-number="formatNumber"
|
||||
/>
|
||||
|
||||
<AdminOverviewSection
|
||||
v-show="activeMenu === 'overview'"
|
||||
v-model:trend-range="trendRange"
|
||||
:trend-polyline="trendPolyline"
|
||||
:trend-markers="trendMarkers"
|
||||
:device-traffic="deviceTraffic"
|
||||
:location-traffic="locationTraffic"
|
||||
/>
|
||||
|
||||
<AdminToolsSection
|
||||
v-show="activeMenu === 'tools'"
|
||||
:tool-filters="consoleStore.toolFilters"
|
||||
:category-loading="consoleStore.categoryLoading"
|
||||
:categories="consoleStore.categories"
|
||||
:status-options="statusOptions"
|
||||
:access-mode-options="accessModeOptions"
|
||||
:tool-loading="consoleStore.toolLoading"
|
||||
:tools="consoleStore.tools"
|
||||
:tool-pagination="consoleStore.toolPagination"
|
||||
:status-tag-type="statusTagType"
|
||||
:access-mode-tag-type="accessModeTagType"
|
||||
:format-number="formatNumber"
|
||||
:format-date="formatDate"
|
||||
@search="searchTools"
|
||||
@reset="resetToolFilters"
|
||||
@create="openCreateToolDialog"
|
||||
@edit="openEditToolDialog"
|
||||
@artifact="openArtifactDialog"
|
||||
@status="openStatusDialog"
|
||||
@mode="openModeDialog"
|
||||
@delete="deleteTool"
|
||||
@page-change="handleToolPageChange"
|
||||
@size-change="handleToolSizeChange"
|
||||
/>
|
||||
|
||||
<AdminAuditSection
|
||||
v-show="activeMenu === 'audit'"
|
||||
:audit-filters="consoleStore.auditFilters"
|
||||
:audit-loading="consoleStore.auditLoading"
|
||||
:audit-logs="consoleStore.auditLogs"
|
||||
:audit-pagination="consoleStore.auditPagination"
|
||||
:format-date-time="formatDateTime"
|
||||
:stringify-body="stringifyBody"
|
||||
@search="searchAuditLogs"
|
||||
@reset="resetAuditFilters"
|
||||
@page-change="handleAuditPageChange"
|
||||
@size-change="handleAuditSizeChange"
|
||||
/>
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" v-bind="currentPageProps" v-on="currentPageEvents" />
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -141,12 +91,24 @@
|
||||
:tool-form-rules="toolFormRules"
|
||||
:categories="consoleStore.categories"
|
||||
:category-loading="consoleStore.categoryLoading"
|
||||
:tags="consoleStore.tags"
|
||||
:tag-loading="consoleStore.tagLoading"
|
||||
:access-mode-options="accessModeOptions"
|
||||
:status-options="statusOptions"
|
||||
:submitting="toolDialog.submitting"
|
||||
@submit="submitToolForm"
|
||||
/>
|
||||
|
||||
<CategoryFormDialog
|
||||
ref="categoryDialogFormRef"
|
||||
v-model:visible="categoryDialog.visible"
|
||||
:mode="categoryDialog.mode"
|
||||
:category-form="categoryForm"
|
||||
:category-form-rules="categoryFormRules"
|
||||
:submitting="categoryDialog.submitting"
|
||||
@submit="submitCategoryForm"
|
||||
/>
|
||||
|
||||
<ArtifactDialog
|
||||
v-model:visible="artifactDialog.visible"
|
||||
:artifact-dialog="artifactDialog"
|
||||
@@ -182,26 +144,34 @@
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getApiErrorMessage } from '../api';
|
||||
import AdminKpiRow from './components/AdminKpiRow.vue';
|
||||
import AdminSidebar from './components/AdminSidebar.vue';
|
||||
import AdminTopbar from './components/AdminTopbar.vue';
|
||||
import AccessModeDialog from './components/AccessModeDialog.vue';
|
||||
import AdminAuditSection from './components/AdminAuditSection.vue';
|
||||
import AdminOverviewSection from './components/AdminOverviewSection.vue';
|
||||
import AdminToolsSection from './components/AdminToolsSection.vue';
|
||||
import ArtifactDialog from './components/ArtifactDialog.vue';
|
||||
import CategoryFormDialog from './components/CategoryFormDialog.vue';
|
||||
import StatusDialog from './components/StatusDialog.vue';
|
||||
import ToolFormDialog from './components/ToolFormDialog.vue';
|
||||
import { useAdminAuthStore } from './stores/auth';
|
||||
import { useAdminConsoleStore } from './stores/console';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const authStore = useAdminAuthStore();
|
||||
const consoleStore = useAdminConsoleStore();
|
||||
|
||||
const activeMenu = ref('overview');
|
||||
const ADMIN_SECTION_ROUTE_MAP = {
|
||||
overview: '/admin/overview',
|
||||
tools: '/admin/tools',
|
||||
categories: '/admin/categories',
|
||||
audit: '/admin/auditlogs',
|
||||
};
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const menuKey = route.meta?.menuKey;
|
||||
return typeof menuKey === 'string' && menuKey ? menuKey : 'tools';
|
||||
});
|
||||
const topSearch = ref('');
|
||||
const trendRange = ref('week');
|
||||
|
||||
@@ -227,14 +197,10 @@ const locationTraffic = [
|
||||
];
|
||||
|
||||
const sectionTitle = computed(() => {
|
||||
if (activeMenu.value === 'tools') {
|
||||
return 'Tools';
|
||||
}
|
||||
if (activeMenu.value === 'audit') {
|
||||
return 'Audit Logs';
|
||||
}
|
||||
return 'Overview';
|
||||
const routeTitle = route.meta?.sectionTitle;
|
||||
return typeof routeTitle === 'string' && routeTitle ? routeTitle : 'Tools';
|
||||
});
|
||||
const isOverviewRoute = computed(() => route.meta?.withKpi === true);
|
||||
|
||||
const kpiCards = computed(() => {
|
||||
const toolTotal = consoleStore.toolPagination.total;
|
||||
@@ -266,6 +232,94 @@ const trendPolyline = computed(() =>
|
||||
.join(' '),
|
||||
);
|
||||
|
||||
const currentPageProps = computed(() => {
|
||||
if (activeMenu.value === 'overview') {
|
||||
return {
|
||||
kpiCards: kpiCards.value,
|
||||
trendRange: trendRange.value,
|
||||
formatNumber,
|
||||
trendPolyline: trendPolyline.value,
|
||||
trendMarkers: trendMarkers.value,
|
||||
deviceTraffic,
|
||||
locationTraffic,
|
||||
};
|
||||
}
|
||||
|
||||
if (activeMenu.value === 'categories') {
|
||||
return {
|
||||
categoryFilters: consoleStore.categoryFilters,
|
||||
categoryLoading: consoleStore.categoryLoading,
|
||||
categories: consoleStore.categories,
|
||||
};
|
||||
}
|
||||
|
||||
if (activeMenu.value === 'audit') {
|
||||
return {
|
||||
auditFilters: consoleStore.auditFilters,
|
||||
auditLoading: consoleStore.auditLoading,
|
||||
auditLogs: consoleStore.auditLogs,
|
||||
auditPagination: consoleStore.auditPagination,
|
||||
formatDateTime,
|
||||
stringifyBody,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
toolFilters: consoleStore.toolFilters,
|
||||
categoryLoading: consoleStore.categoryLoading,
|
||||
categories: consoleStore.categories,
|
||||
statusOptions,
|
||||
accessModeOptions,
|
||||
toolLoading: consoleStore.toolLoading,
|
||||
tools: consoleStore.tools,
|
||||
toolPagination: consoleStore.toolPagination,
|
||||
statusTagType,
|
||||
accessModeTagType,
|
||||
formatNumber,
|
||||
formatDate,
|
||||
};
|
||||
});
|
||||
|
||||
const currentPageEvents = computed(() => {
|
||||
if (activeMenu.value === 'overview') {
|
||||
return {
|
||||
'update:trend-range': updateTrendRange,
|
||||
};
|
||||
}
|
||||
|
||||
if (activeMenu.value === 'categories') {
|
||||
return {
|
||||
search: searchCategories,
|
||||
reset: resetCategoryFilters,
|
||||
create: openCreateCategoryDialog,
|
||||
edit: openEditCategoryDialog,
|
||||
delete: deleteCategory,
|
||||
};
|
||||
}
|
||||
|
||||
if (activeMenu.value === 'audit') {
|
||||
return {
|
||||
search: searchAuditLogs,
|
||||
reset: resetAuditFilters,
|
||||
'page-change': handleAuditPageChange,
|
||||
'size-change': handleAuditSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
search: searchTools,
|
||||
reset: resetToolFilters,
|
||||
create: openCreateToolDialog,
|
||||
edit: openEditToolDialog,
|
||||
artifact: openArtifactDialog,
|
||||
status: openStatusDialog,
|
||||
mode: openModeDialog,
|
||||
delete: deleteTool,
|
||||
'page-change': handleToolPageChange,
|
||||
'size-change': handleToolSizeChange,
|
||||
};
|
||||
});
|
||||
|
||||
const loginFormRef = ref(null);
|
||||
const loginForm = reactive({
|
||||
username: 'admin',
|
||||
@@ -295,11 +349,13 @@ const accessModeOptions = [
|
||||
];
|
||||
|
||||
const toolDialogFormRef = ref(null);
|
||||
const categoryDialogFormRef = ref(null);
|
||||
|
||||
function createEmptyToolForm() {
|
||||
return {
|
||||
name: '',
|
||||
categoryId: '',
|
||||
tagIds: [],
|
||||
description: '',
|
||||
rating: 0,
|
||||
featuresText: '',
|
||||
@@ -318,6 +374,18 @@ const toolFormRules = {
|
||||
{ min: 2, max: 120, message: '工具名称长度为 2-120 位', trigger: 'blur' },
|
||||
],
|
||||
categoryId: [{ required: true, message: '请选择工具分类', trigger: 'change' }],
|
||||
tagIds: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (Array.isArray(value) && value.length > 20) {
|
||||
callback(new Error('最多选择 20 个标签'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入工具简介', trigger: 'blur' },
|
||||
{ min: 10, max: 2000, message: '工具简介长度为 10-2000 位', trigger: 'blur' },
|
||||
@@ -358,6 +426,43 @@ const toolDialog = reactive({
|
||||
submitting: false,
|
||||
});
|
||||
|
||||
function createEmptyCategoryForm() {
|
||||
return {
|
||||
name: '',
|
||||
sortOrder: 100,
|
||||
};
|
||||
}
|
||||
|
||||
const categoryForm = reactive(createEmptyCategoryForm());
|
||||
|
||||
const categoryFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 1, max: 80, message: '分类名称长度为 1-80 位', trigger: 'blur' },
|
||||
],
|
||||
sortOrder: [
|
||||
{ required: true, message: '请输入排序值', trigger: 'change' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isInteger(numeric) || numeric < 0 || numeric > 9999) {
|
||||
callback(new Error('排序值必须是 0-9999 的整数'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const categoryDialog = reactive({
|
||||
visible: false,
|
||||
mode: 'create',
|
||||
id: '',
|
||||
submitting: false,
|
||||
});
|
||||
|
||||
const artifactDialog = reactive({
|
||||
visible: false,
|
||||
toolId: '',
|
||||
@@ -409,6 +514,10 @@ function calcTrendPoint(index) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function updateTrendRange(nextRange) {
|
||||
trendRange.value = nextRange;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const numeric = Number(value);
|
||||
return new Intl.NumberFormat('zh-CN').format(Number.isFinite(numeric) ? numeric : 0);
|
||||
@@ -522,10 +631,85 @@ function normalizeFeatureText(value) {
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
function normalizeTagName(value) {
|
||||
return String(value || '').trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function splitTagSelections(values) {
|
||||
const selected = Array.isArray(values) ? values : [];
|
||||
const sourceTags = Array.isArray(consoleStore.tags) ? consoleStore.tags : [];
|
||||
const knownTagIdSet = new Set(sourceTags.map((item) => item.id));
|
||||
const knownTagNameToId = new Map(
|
||||
sourceTags
|
||||
.map((item) => [normalizeTagName(item.name).toLowerCase(), item.id])
|
||||
.filter(([name]) => Boolean(name)),
|
||||
);
|
||||
|
||||
const existingTagIds = [];
|
||||
const newTagNames = [];
|
||||
const seenTagIds = new Set();
|
||||
const seenTagNames = new Set();
|
||||
|
||||
selected.forEach((item) => {
|
||||
const raw = String(item ?? '');
|
||||
const normalized = normalizeTagName(raw);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (knownTagIdSet.has(raw)) {
|
||||
if (!seenTagIds.has(raw)) {
|
||||
existingTagIds.push(raw);
|
||||
seenTagIds.add(raw);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIdByName = knownTagNameToId.get(normalized.toLowerCase());
|
||||
if (existingIdByName) {
|
||||
if (!seenTagIds.has(existingIdByName)) {
|
||||
existingTagIds.push(existingIdByName);
|
||||
seenTagIds.add(existingIdByName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seenTagNames.has(normalized.toLowerCase())) {
|
||||
newTagNames.push(normalized);
|
||||
seenTagNames.add(normalized.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
existingTagIds,
|
||||
newTagNames,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveToolTagIds(token) {
|
||||
const { existingTagIds, newTagNames } = splitTagSelections(toolForm.tagIds);
|
||||
if (newTagNames.length === 0) {
|
||||
return existingTagIds;
|
||||
}
|
||||
|
||||
const createdTags = await Promise.all(
|
||||
newTagNames.map((name) => consoleStore.createTag({ name }, token)),
|
||||
);
|
||||
const createdTagIds = createdTags
|
||||
.map((item) => item?.id)
|
||||
.filter((id) => typeof id === 'string' && id);
|
||||
|
||||
return Array.from(new Set([...existingTagIds, ...createdTagIds]));
|
||||
}
|
||||
|
||||
function resetToolForm() {
|
||||
Object.assign(toolForm, createEmptyToolForm());
|
||||
}
|
||||
|
||||
function resetCategoryForm() {
|
||||
Object.assign(categoryForm, createEmptyCategoryForm());
|
||||
}
|
||||
|
||||
function resetArtifactForm() {
|
||||
artifactForm.version = '';
|
||||
artifactForm.releaseNotes = '';
|
||||
@@ -550,7 +734,8 @@ async function runWithAuth(fn) {
|
||||
async function initializeAdminData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
consoleStore.loadCategories(),
|
||||
runWithAuth((token) => consoleStore.loadCategories(token)),
|
||||
runWithAuth((token) => consoleStore.loadTags(token)),
|
||||
runWithAuth((token) => consoleStore.loadTools(token)),
|
||||
runWithAuth((token) => consoleStore.loadAuditLogs(token)),
|
||||
]);
|
||||
@@ -594,23 +779,30 @@ function goPublic() {
|
||||
}
|
||||
|
||||
function switchMenu(nextKey) {
|
||||
if (!['overview', 'tools', 'audit'].includes(nextKey)) {
|
||||
const targetPath = ADMIN_SECTION_ROUTE_MAP[nextKey];
|
||||
if (!targetPath) {
|
||||
ElMessage.info('该菜单为展示项,当前版本暂未开放');
|
||||
return;
|
||||
}
|
||||
activeMenu.value = nextKey;
|
||||
if (route.path !== targetPath) {
|
||||
router.push(targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
function openOverviewSection() {
|
||||
activeMenu.value = 'overview';
|
||||
switchMenu('overview');
|
||||
}
|
||||
|
||||
function openToolsSection() {
|
||||
activeMenu.value = 'tools';
|
||||
switchMenu('tools');
|
||||
}
|
||||
|
||||
function openCategoriesSection() {
|
||||
switchMenu('categories');
|
||||
}
|
||||
|
||||
function openAuditSection() {
|
||||
activeMenu.value = 'audit';
|
||||
switchMenu('audit');
|
||||
}
|
||||
|
||||
async function refreshCurrentSection() {
|
||||
@@ -618,11 +810,15 @@ async function refreshCurrentSection() {
|
||||
await loadTools();
|
||||
return;
|
||||
}
|
||||
if (activeMenu.value === 'categories') {
|
||||
await loadCategories();
|
||||
return;
|
||||
}
|
||||
if (activeMenu.value === 'audit') {
|
||||
await loadAuditLogs();
|
||||
return;
|
||||
}
|
||||
await Promise.all([loadTools(), loadAuditLogs()]);
|
||||
await Promise.all([loadCategories(), loadTools(), loadAuditLogs()]);
|
||||
}
|
||||
|
||||
function applyTopSearch() {
|
||||
@@ -637,10 +833,26 @@ function applyTopSearch() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeMenu.value === 'categories') {
|
||||
consoleStore.categoryFilters.query = value;
|
||||
searchCategories();
|
||||
return;
|
||||
}
|
||||
|
||||
consoleStore.toolFilters.query = value;
|
||||
searchTools();
|
||||
}
|
||||
|
||||
async function searchCategories() {
|
||||
await loadCategories();
|
||||
}
|
||||
|
||||
async function resetCategoryFilters() {
|
||||
consoleStore.resetCategoryFilters();
|
||||
topSearch.value = '';
|
||||
await loadCategories();
|
||||
}
|
||||
|
||||
async function searchTools() {
|
||||
consoleStore.toolFilters.page = 1;
|
||||
await loadTools();
|
||||
@@ -663,6 +875,20 @@ async function handleToolSizeChange(size) {
|
||||
await loadTools();
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
await runWithAuth((token) => consoleStore.loadCategories(token));
|
||||
} catch (error) {
|
||||
if (isUnauthorized(error)) {
|
||||
await authStore.logout();
|
||||
consoleStore.$reset();
|
||||
ElMessage.error('登录已过期,请重新登录');
|
||||
return;
|
||||
}
|
||||
ElMessage.error(getApiErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTools() {
|
||||
try {
|
||||
await runWithAuth((token) => consoleStore.loadTools(token));
|
||||
@@ -677,6 +903,81 @@ async function loadTools() {
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateCategoryDialog() {
|
||||
categoryDialog.mode = 'create';
|
||||
categoryDialog.id = '';
|
||||
resetCategoryForm();
|
||||
categoryDialog.visible = true;
|
||||
}
|
||||
|
||||
function openEditCategoryDialog(row) {
|
||||
categoryDialog.mode = 'edit';
|
||||
categoryDialog.id = row.id;
|
||||
Object.assign(categoryForm, {
|
||||
name: row.name || '',
|
||||
sortOrder: Number(row.sortOrder ?? 100),
|
||||
});
|
||||
categoryDialog.visible = true;
|
||||
}
|
||||
|
||||
function buildCategoryPayload() {
|
||||
return {
|
||||
name: categoryForm.name.trim(),
|
||||
sortOrder: Number(categoryForm.sortOrder ?? 100),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitCategoryForm() {
|
||||
const formRef = categoryDialogFormRef.value;
|
||||
if (!formRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
categoryDialog.submitting = true;
|
||||
try {
|
||||
const payload = buildCategoryPayload();
|
||||
await runWithAuth((token) => {
|
||||
if (categoryDialog.mode === 'create') {
|
||||
return consoleStore.createCategory(payload, token);
|
||||
}
|
||||
return consoleStore.updateCategory(categoryDialog.id, payload, token);
|
||||
});
|
||||
categoryDialog.visible = false;
|
||||
ElMessage.success(categoryDialog.mode === 'create' ? '分类已创建' : '分类已更新');
|
||||
await Promise.all([loadCategories(), loadTools()]);
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error));
|
||||
} finally {
|
||||
categoryDialog.submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCategory(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除分类「${row.name}」吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await runWithAuth((token) => consoleStore.deleteCategory(row.id, token));
|
||||
ElMessage.success('分类已删除');
|
||||
await Promise.all([loadCategories(), loadTools()]);
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateToolDialog() {
|
||||
toolDialog.mode = 'create';
|
||||
toolDialog.id = '';
|
||||
@@ -690,6 +991,7 @@ function openEditToolDialog(row) {
|
||||
Object.assign(toolForm, {
|
||||
name: row.name || '',
|
||||
categoryId: row.category?.id || '',
|
||||
tagIds: Array.isArray(row.tags) ? row.tags.map((item) => item.id).filter(Boolean) : [],
|
||||
description: row.description || '',
|
||||
rating: Number(row.rating ?? 0),
|
||||
featuresText: Array.isArray(row.features) ? row.features.join('\n') : '',
|
||||
@@ -701,10 +1003,11 @@ function openEditToolDialog(row) {
|
||||
toolDialog.visible = true;
|
||||
}
|
||||
|
||||
function buildToolPayload() {
|
||||
function buildToolPayload(tagIds) {
|
||||
const payload = {
|
||||
name: toolForm.name.trim(),
|
||||
categoryId: toolForm.categoryId,
|
||||
tags: tagIds,
|
||||
description: toolForm.description.trim(),
|
||||
rating: Number(toolForm.rating ?? 0),
|
||||
features: normalizeFeatureText(toolForm.featuresText),
|
||||
@@ -736,8 +1039,9 @@ async function submitToolForm() {
|
||||
|
||||
toolDialog.submitting = true;
|
||||
try {
|
||||
const payload = buildToolPayload();
|
||||
await runWithAuth((token) => {
|
||||
await runWithAuth(async (token) => {
|
||||
const resolvedTagIds = await resolveToolTagIds(token);
|
||||
const payload = buildToolPayload(resolvedTagIds);
|
||||
if (toolDialog.mode === 'create') {
|
||||
return consoleStore.createTool(payload, token);
|
||||
}
|
||||
@@ -998,6 +1302,9 @@ async function loadAuditLogs() {
|
||||
}
|
||||
|
||||
watch(activeMenu, async (nextMenu) => {
|
||||
if (nextMenu === 'categories' && !consoleStore.categories.length) {
|
||||
await loadCategories();
|
||||
}
|
||||
if (nextMenu === 'tools' && !consoleStore.tools.length) {
|
||||
await loadTools();
|
||||
}
|
||||
@@ -1025,6 +1332,16 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => categoryDialog.visible,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
resetCategoryForm();
|
||||
categoryDialogFormRef.value?.clearValidate?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => artifactDialog.visible,
|
||||
(visible) => {
|
||||
|
||||
@@ -463,6 +463,10 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.category-filters {
|
||||
grid-template-columns: 1.2fr auto auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,32 @@ export async function adminDeleteArtifact(toolId, artifactId, token) {
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminGetCategories() {
|
||||
const response = await http.get('/categories');
|
||||
export async function adminGetCategories(params, token) {
|
||||
const response = await http.get('/admin/categories', withToken(token, { params }));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminCreateCategory(payload, token) {
|
||||
const response = await http.post('/admin/categories', payload, withToken(token));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminUpdateCategory(id, payload, token) {
|
||||
const response = await http.patch(`/admin/categories/${id}`, payload, withToken(token));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminDeleteCategory(id, token) {
|
||||
const response = await http.delete(`/admin/categories/${id}`, withToken(token));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminGetTags(token) {
|
||||
const response = await http.get('/admin/tags', withToken(token));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
export async function adminCreateTag(payload, token) {
|
||||
const response = await http.post('/admin/tags', payload, withToken(token));
|
||||
return unwrap(response.data);
|
||||
}
|
||||
|
||||
69
client/src/admin/components/AdminCategoriesSection.vue
Normal file
69
client/src/admin/components/AdminCategoriesSection.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<section class="dashboard-section">
|
||||
<article class="panel data-panel">
|
||||
<header class="data-head">
|
||||
<div>
|
||||
<h3>Category Management</h3>
|
||||
<p>支持工具分类新增、编辑与删除维护</p>
|
||||
</div>
|
||||
<div class="data-head-actions">
|
||||
<el-button type="primary" @click="emit('create')">新增分类</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="data-filters category-filters">
|
||||
<el-input
|
||||
v-model="categoryFilters.query"
|
||||
placeholder="搜索分类名称"
|
||||
clearable
|
||||
@keyup.enter="emit('search')"
|
||||
/>
|
||||
<el-button type="primary" :loading="categoryLoading" @click="emit('search')">查询</el-button>
|
||||
<el-button @click="emit('reset')">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="categories"
|
||||
border
|
||||
stripe
|
||||
v-loading="categoryLoading"
|
||||
class="data-table"
|
||||
>
|
||||
<el-table-column prop="name" label="分类名称" min-width="220" />
|
||||
<el-table-column prop="sortOrder" label="排序值" width="120" />
|
||||
<el-table-column label="工具数" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ Number(row.toolCount || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-space>
|
||||
<el-button size="small" type="primary" plain @click="emit('edit', row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="emit('delete', row)">删除</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
categoryFilters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
categoryLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'reset', 'create', 'edit', 'delete']);
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<aside class="dashboard-sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<span class="brand-mark">*</span>
|
||||
<span class="brand-text">snowui</span>
|
||||
<span class="brand-text">管理端</span>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-menu">
|
||||
@@ -31,6 +31,7 @@
|
||||
<script setup>
|
||||
import {
|
||||
ChatDotRound,
|
||||
CollectionTag,
|
||||
DataAnalysis,
|
||||
Document,
|
||||
Management,
|
||||
@@ -50,11 +51,12 @@ const emit = defineEmits(['menu-change', 'go-public', 'logout']);
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'overview', label: 'Overview', icon: DataAnalysis },
|
||||
{ key: 'tools', label: 'Tool Management', icon: ShoppingBag },
|
||||
{ key: 'audit', label: 'Audit Logs', icon: Document },
|
||||
{ key: 'projects', label: 'Projects', icon: Management },
|
||||
{ key: 'profile', label: 'User Profile', icon: User },
|
||||
{ key: 'account', label: 'Account', icon: Setting },
|
||||
{ key: 'social', label: 'Social', icon: ChatDotRound },
|
||||
{ key: 'tools', label: '工具管理', icon: ShoppingBag },
|
||||
{ key: 'categories', label: '分类管理', icon: CollectionTag },
|
||||
{ key: 'audit', label: '审计日志', icon: Document },
|
||||
// { key: 'projects', label: 'Projects', icon: Management },
|
||||
// { key: 'profile', label: 'User Profile', icon: User },
|
||||
// { key: 'account', label: 'Account', icon: Setting },
|
||||
// { key: 'social', label: 'Social', icon: ChatDotRound },
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
<el-button class="icon-btn" circle @click="emit('open-tools')">
|
||||
<el-icon><Grid /></el-icon>
|
||||
</el-button>
|
||||
<el-button class="icon-btn" circle @click="emit('open-categories')">
|
||||
<el-icon><CollectionTag /></el-icon>
|
||||
</el-button>
|
||||
<el-button class="icon-btn" circle @click="emit('open-audit')">
|
||||
<el-icon><Document /></el-icon>
|
||||
</el-button>
|
||||
@@ -36,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DataAnalysis, Document, Grid, Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { CollectionTag, DataAnalysis, Document, Grid, Refresh, Search } from '@element-plus/icons-vue';
|
||||
|
||||
defineProps({
|
||||
sectionTitle: {
|
||||
@@ -50,5 +53,12 @@ const search = defineModel('search', {
|
||||
default: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['apply-search', 'refresh', 'open-overview', 'open-tools', 'open-audit']);
|
||||
const emit = defineEmits([
|
||||
'apply-search',
|
||||
'refresh',
|
||||
'open-overview',
|
||||
'open-tools',
|
||||
'open-categories',
|
||||
'open-audit',
|
||||
]);
|
||||
</script>
|
||||
|
||||
75
client/src/admin/components/CategoryFormDialog.vue
Normal file
75
client/src/admin/components/CategoryFormDialog.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="mode === 'create' ? '新增分类' : '编辑分类'"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="categoryFormRef"
|
||||
:model="categoryForm"
|
||||
:rules="categoryFormRules"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input
|
||||
v-model="categoryForm.name"
|
||||
placeholder="请输入分类名称"
|
||||
maxlength="80"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number
|
||||
v-model="categoryForm.sortOrder"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:step="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="emit('submit')">
|
||||
{{ mode === 'create' ? '创建分类' : '保存修改' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const visible = defineModel('visible', {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
categoryForm: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
categoryFormRules: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
submitting: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
const categoryFormRef = ref(null);
|
||||
|
||||
defineExpose({
|
||||
validate: () => categoryFormRef.value?.validate?.(),
|
||||
clearValidate: () => categoryFormRef.value?.clearValidate?.(),
|
||||
});
|
||||
</script>
|
||||
@@ -30,6 +30,26 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签" prop="tagIds">
|
||||
<el-select
|
||||
v-model="toolForm.tagIds"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:reserve-keyword="false"
|
||||
placeholder="可多选,也可直接输入新标签"
|
||||
style="width: 100%"
|
||||
:loading="tagLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tags"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="工具简介" prop="description">
|
||||
<el-input
|
||||
v-model="toolForm.description"
|
||||
@@ -116,6 +136,14 @@ defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tagLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
accessModeOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
||||
47
client/src/admin/pages/AdminAuditLogsPage.vue
Normal file
47
client/src/admin/pages/AdminAuditLogsPage.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<AdminAuditSection
|
||||
:audit-filters="auditFilters"
|
||||
:audit-loading="auditLoading"
|
||||
:audit-logs="auditLogs"
|
||||
:audit-pagination="auditPagination"
|
||||
:format-date-time="formatDateTime"
|
||||
:stringify-body="stringifyBody"
|
||||
@search="emit('search')"
|
||||
@reset="emit('reset')"
|
||||
@page-change="emit('page-change', $event)"
|
||||
@size-change="emit('size-change', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminAuditSection from '../components/AdminAuditSection.vue';
|
||||
|
||||
defineProps({
|
||||
auditFilters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
auditLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
auditLogs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
auditPagination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formatDateTime: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
stringifyBody: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'reset', 'page-change', 'size-change']);
|
||||
</script>
|
||||
33
client/src/admin/pages/AdminCategoriesPage.vue
Normal file
33
client/src/admin/pages/AdminCategoriesPage.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<AdminCategoriesSection
|
||||
:category-filters="categoryFilters"
|
||||
:category-loading="categoryLoading"
|
||||
:categories="categories"
|
||||
@search="emit('search')"
|
||||
@reset="emit('reset')"
|
||||
@create="emit('create')"
|
||||
@edit="emit('edit', $event)"
|
||||
@delete="emit('delete', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminCategoriesSection from '../components/AdminCategoriesSection.vue';
|
||||
|
||||
defineProps({
|
||||
categoryFilters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
categoryLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'reset', 'create', 'edit', 'delete']);
|
||||
</script>
|
||||
52
client/src/admin/pages/AdminOverviewPage.vue
Normal file
52
client/src/admin/pages/AdminOverviewPage.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<section class="admin-page-overview">
|
||||
<AdminKpiRow :kpi-cards="kpiCards" :format-number="formatNumber" />
|
||||
|
||||
<AdminOverviewSection
|
||||
:trend-range="trendRange"
|
||||
:trend-polyline="trendPolyline"
|
||||
:trend-markers="trendMarkers"
|
||||
:device-traffic="deviceTraffic"
|
||||
:location-traffic="locationTraffic"
|
||||
@update:trend-range="emit('update:trend-range', $event)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminKpiRow from '../components/AdminKpiRow.vue';
|
||||
import AdminOverviewSection from '../components/AdminOverviewSection.vue';
|
||||
|
||||
defineProps({
|
||||
kpiCards: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
trendRange: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
formatNumber: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
trendPolyline: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
trendMarkers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
deviceTraffic: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
locationTraffic: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:trend-range']);
|
||||
</script>
|
||||
94
client/src/admin/pages/AdminToolsPage.vue
Normal file
94
client/src/admin/pages/AdminToolsPage.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<AdminToolsSection
|
||||
:tool-filters="toolFilters"
|
||||
:category-loading="categoryLoading"
|
||||
:categories="categories"
|
||||
:status-options="statusOptions"
|
||||
:access-mode-options="accessModeOptions"
|
||||
:tool-loading="toolLoading"
|
||||
:tools="tools"
|
||||
:tool-pagination="toolPagination"
|
||||
:status-tag-type="statusTagType"
|
||||
:access-mode-tag-type="accessModeTagType"
|
||||
:format-number="formatNumber"
|
||||
:format-date="formatDate"
|
||||
@search="emit('search')"
|
||||
@reset="emit('reset')"
|
||||
@create="emit('create')"
|
||||
@edit="emit('edit', $event)"
|
||||
@artifact="emit('artifact', $event)"
|
||||
@status="emit('status', $event)"
|
||||
@mode="emit('mode', $event)"
|
||||
@delete="emit('delete', $event)"
|
||||
@page-change="emit('page-change', $event)"
|
||||
@size-change="emit('size-change', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminToolsSection from '../components/AdminToolsSection.vue';
|
||||
|
||||
defineProps({
|
||||
toolFilters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
categoryLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
statusOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
accessModeOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
toolLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
tools: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
toolPagination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
statusTagType: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
accessModeTagType: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
formatNumber: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
formatDate: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'search',
|
||||
'reset',
|
||||
'create',
|
||||
'edit',
|
||||
'artifact',
|
||||
'status',
|
||||
'mode',
|
||||
'delete',
|
||||
'page-change',
|
||||
'size-change',
|
||||
]);
|
||||
</script>
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import PublicApp from '../App.vue';
|
||||
import AdminApp from './AdminApp.vue';
|
||||
import AdminAuditLogsPage from './pages/AdminAuditLogsPage.vue';
|
||||
import AdminCategoriesPage from './pages/AdminCategoriesPage.vue';
|
||||
import AdminOverviewPage from './pages/AdminOverviewPage.vue';
|
||||
import AdminToolsPage from './pages/AdminToolsPage.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -10,8 +14,50 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin-home',
|
||||
component: AdminApp,
|
||||
redirect: '/admin/tools',
|
||||
children: [
|
||||
{
|
||||
path: 'overview',
|
||||
name: 'admin-overview',
|
||||
component: AdminOverviewPage,
|
||||
meta: {
|
||||
menuKey: 'overview',
|
||||
sectionTitle: 'Overview',
|
||||
withKpi: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tools',
|
||||
name: 'admin-tools',
|
||||
component: AdminToolsPage,
|
||||
meta: {
|
||||
menuKey: 'tools',
|
||||
sectionTitle: 'Tools',
|
||||
withKpi: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'admin-categories',
|
||||
component: AdminCategoriesPage,
|
||||
meta: {
|
||||
menuKey: 'categories',
|
||||
sectionTitle: 'Categories',
|
||||
withKpi: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'auditlogs',
|
||||
name: 'admin-auditlogs',
|
||||
component: AdminAuditLogsPage,
|
||||
meta: {
|
||||
menuKey: 'audit',
|
||||
sectionTitle: 'Audit Logs',
|
||||
withKpi: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
adminCreateCategory,
|
||||
adminCreateTag,
|
||||
adminCreateTool,
|
||||
adminDeleteCategory,
|
||||
adminDeleteArtifact,
|
||||
adminDeleteTool,
|
||||
adminGetArtifacts,
|
||||
adminGetAuditLogs,
|
||||
adminGetCategories,
|
||||
adminGetTags,
|
||||
adminGetTools,
|
||||
adminSetLatestArtifact,
|
||||
adminUpdateArtifactStatus,
|
||||
adminUpdateCategory,
|
||||
adminUpdateTool,
|
||||
adminUpdateAccessMode,
|
||||
adminUpdateToolStatus,
|
||||
@@ -19,6 +24,11 @@ export const useAdminConsoleStore = defineStore('admin-console', {
|
||||
state: () => ({
|
||||
categories: [],
|
||||
categoryLoading: false,
|
||||
categoryFilters: {
|
||||
query: '',
|
||||
},
|
||||
tags: [],
|
||||
tagLoading: false,
|
||||
|
||||
toolFilters: {
|
||||
query: '',
|
||||
@@ -54,15 +64,57 @@ export const useAdminConsoleStore = defineStore('admin-console', {
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
async loadCategories() {
|
||||
async loadCategories(token) {
|
||||
this.categoryLoading = true;
|
||||
try {
|
||||
const data = await adminGetCategories();
|
||||
const data = await adminGetCategories(
|
||||
{
|
||||
query: this.categoryFilters.query || undefined,
|
||||
},
|
||||
token,
|
||||
);
|
||||
this.categories = Array.isArray(data) ? data : [];
|
||||
} finally {
|
||||
this.categoryLoading = false;
|
||||
}
|
||||
},
|
||||
resetCategoryFilters() {
|
||||
this.categoryFilters.query = '';
|
||||
},
|
||||
async createCategory(payload, token) {
|
||||
return adminCreateCategory(payload, token);
|
||||
},
|
||||
async updateCategory(id, payload, token) {
|
||||
return adminUpdateCategory(id, payload, token);
|
||||
},
|
||||
async deleteCategory(id, token) {
|
||||
return adminDeleteCategory(id, token);
|
||||
},
|
||||
async loadTags(token) {
|
||||
this.tagLoading = true;
|
||||
try {
|
||||
const data = await adminGetTags(token);
|
||||
this.tags = Array.isArray(data) ? data : [];
|
||||
} finally {
|
||||
this.tagLoading = false;
|
||||
}
|
||||
},
|
||||
async createTag(payload, token) {
|
||||
const created = await adminCreateTag(payload, token);
|
||||
const createdId = created?.id;
|
||||
if (!createdId) {
|
||||
return created;
|
||||
}
|
||||
|
||||
const existingIndex = this.tags.findIndex((item) => item.id === createdId);
|
||||
if (existingIndex >= 0) {
|
||||
this.tags.splice(existingIndex, 1, created);
|
||||
} else {
|
||||
this.tags.push(created);
|
||||
}
|
||||
this.tags.sort((a, b) => String(a?.name ?? '').localeCompare(String(b?.name ?? '')));
|
||||
return created;
|
||||
},
|
||||
setToolPage(page) {
|
||||
this.toolFilters.page = page;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user