修复知识库的标签管理和分类管理
This commit is contained in:
parent
494e0160b7
commit
7972f0a6d9
@ -205,36 +205,19 @@ const transformMenuData = (menus) => {
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 优先从 localStorage 读取
|
||||
const cachedMenus = localStorage.getItem('menuData');
|
||||
let menuData = null;
|
||||
|
||||
if (cachedMenus) {
|
||||
try {
|
||||
menuData = JSON.parse(cachedMenus);
|
||||
} catch (e) {
|
||||
console.warn('缓存菜单数据解析失败:', e);
|
||||
}
|
||||
// 直接从接口获取菜单数据,不使用缓存
|
||||
const res = await getAllMenus();
|
||||
if (res && res.success && res.data) {
|
||||
const menuData = res.data;
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
list.value = transformedMenus;
|
||||
} else {
|
||||
console.error('获取菜单失败:', res?.message || '未知错误');
|
||||
list.value = [];
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果缓存中没有或解析失败,从接口获取
|
||||
if (!menuData) {
|
||||
const res = await getAllMenus();
|
||||
if (res && res.success && res.data) {
|
||||
menuData = res.data;
|
||||
// 保存到缓存
|
||||
localStorage.setItem('menuData', JSON.stringify(menuData));
|
||||
} else {
|
||||
console.error('获取菜单失败:', res?.message || '未知错误');
|
||||
list.value = [];
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
list.value = transformedMenus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
list.value = [];
|
||||
|
||||
@ -130,11 +130,6 @@ const handleCommand = (command) => {
|
||||
//清除租户数据
|
||||
localStorage.removeItem('tenant');
|
||||
sessionStorage.removeItem('tenant');
|
||||
//清除菜单数据
|
||||
localStorage.removeItem('menus');
|
||||
sessionStorage.removeItem('menus');
|
||||
localStorage.removeItem('menuData');
|
||||
sessionStorage.removeItem('menuData');
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
@ -42,16 +42,30 @@ export function convertMenusToRoutes(menus) {
|
||||
// 子菜单添加到父菜单的 children 中
|
||||
const parentRoute = menuMap[menu.parentId];
|
||||
if (parentRoute) {
|
||||
// 修正子路由路径:相对于父路由的路径
|
||||
const parentPath = parentRoute.path; // 例如 "system"
|
||||
const childFullPath = currentRoute.path; // 例如 "system/users"
|
||||
// 修正子路由路径:使用原始菜单路径来准确匹配
|
||||
// 使用原始路径(menuPath)来准确计算相对路径
|
||||
const parentOriginalPath = parentRoute.meta?.menuPath || parentRoute.path;
|
||||
const childOriginalPath = currentRoute.meta?.menuPath || currentRoute.path;
|
||||
|
||||
// 确保路径格式一致(去掉前导斜杠)
|
||||
const parentPath = parentOriginalPath.startsWith('/') ? parentOriginalPath.substring(1) : parentOriginalPath;
|
||||
const childFullPath = childOriginalPath.startsWith('/') ? childOriginalPath.substring(1) : childOriginalPath;
|
||||
|
||||
// 如果子路径以父路径开头,则只保留剩余部分
|
||||
if (childFullPath.startsWith(parentPath + '/')) {
|
||||
currentRoute.path = childFullPath.substring(parentPath.length + 1); // "users"
|
||||
} else if (childFullPath.startsWith('/')) {
|
||||
// 如果仍然有前导斜杠,去掉它
|
||||
currentRoute.path = childFullPath.substring(1);
|
||||
currentRoute.path = childFullPath.substring(parentPath.length + 1); // "users" 或 "knowledge/category"
|
||||
} else {
|
||||
// 如果子路径不以父路径开头,尝试直接使用子路径的最后一部分
|
||||
// 这处理了路径不连续的情况
|
||||
const pathParts = childFullPath.split('/');
|
||||
const parentPathParts = parentPath.split('/');
|
||||
// 找到子路径相对于父路径的部分
|
||||
if (pathParts.length > parentPathParts.length) {
|
||||
currentRoute.path = pathParts.slice(parentPathParts.length).join('/');
|
||||
} else {
|
||||
// 如果无法确定相对路径,使用最后一个路径段
|
||||
currentRoute.path = pathParts[pathParts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentRoute.children) {
|
||||
@ -64,6 +78,29 @@ export function convertMenusToRoutes(menus) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2.5. 为有子路由但没有 component 的父路由自动添加默认组件
|
||||
const addDefaultComponent = (routeList) => {
|
||||
routeList.forEach(route => {
|
||||
// 如果路由有子路由但没有组件,尝试添加默认组件
|
||||
if (route.children && route.children.length > 0 && !route.component) {
|
||||
// 使用原始菜单路径(menuPath)来推断组件路径,而不是修正后的路径
|
||||
const originalPath = route.meta?.menuPath || route.path;
|
||||
// 确保路径以 / 开头
|
||||
const normalizedPath = originalPath.startsWith('/') ? originalPath : `/${originalPath}`;
|
||||
const defaultComponentPath = `${normalizedPath}/index.vue`;
|
||||
const defaultComponent = createComponentLoader(defaultComponentPath);
|
||||
if (defaultComponent) {
|
||||
route.component = defaultComponent;
|
||||
}
|
||||
}
|
||||
// 递归处理子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
addDefaultComponent(route.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
addDefaultComponent(routes);
|
||||
|
||||
// 3. 按 order 排序
|
||||
const sortRoutes = (routesList) => {
|
||||
|
||||
@ -142,24 +142,24 @@ function addDynamicRoutes(menus) {
|
||||
|
||||
// 添加知识库的子路由(详情页和编辑页)
|
||||
// 直接在 Main 路由下添加完整路径的子路由
|
||||
router.addRoute('Main', {
|
||||
path: 'apps/knowledge/detail/:id',
|
||||
name: 'apps-knowledge-detail',
|
||||
component: () => import('@/views/apps/knowledge/components/detail.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '知识详情'
|
||||
}
|
||||
});
|
||||
router.addRoute('Main', {
|
||||
path: 'apps/knowledge/edit/:id',
|
||||
name: 'apps-knowledge-edit',
|
||||
component: () => import('@/views/apps/knowledge/components/edit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '编辑知识'
|
||||
}
|
||||
});
|
||||
// router.addRoute('Main', {
|
||||
// path: 'apps/knowledge/detail/:id',
|
||||
// name: 'apps-knowledge-detail',
|
||||
// component: () => import('@/views/apps/knowledge/components/detail.vue'),
|
||||
// meta: {
|
||||
// requiresAuth: true,
|
||||
// title: '知识详情'
|
||||
// }
|
||||
// });
|
||||
// router.addRoute('Main', {
|
||||
// path: 'apps/knowledge/edit/:id',
|
||||
// name: 'apps-knowledge-edit',
|
||||
// component: () => import('@/views/apps/knowledge/components/edit.vue'),
|
||||
// meta: {
|
||||
// requiresAuth: true,
|
||||
// title: '编辑知识'
|
||||
// }
|
||||
// });
|
||||
|
||||
dynamicRoutesAdded = true;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
|
||||
// Apps 父路由组件,用于显示子路由
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Apps 父路由容器 */
|
||||
</style>
|
||||
|
||||
@ -1,29 +1,139 @@
|
||||
<template>
|
||||
<div class="category-manage">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">分类管理</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
<div class="header-left">
|
||||
<h2 class="page-title">
|
||||
<el-icon class="title-icon"><Folder /></el-icon>
|
||||
分类管理
|
||||
</h2>
|
||||
<p class="page-desc">管理知识库的分类,方便组织和检索内容</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="fetchList" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-cards">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="分类总数">
|
||||
<el-tag type="primary">{{ categoryList.length }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索分类名称"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="max-width: 300px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 分类内容 -->
|
||||
<div class="category-content">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="categoryList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="categoryId" label="ID" width="80" />
|
||||
<el-table-column prop="categoryName" label="分类名称" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-card shadow="never" v-loading="loading">
|
||||
<!-- 树形表格视图 -->
|
||||
<div v-if="treeCategoryList.length > 0" class="table-container">
|
||||
<el-table
|
||||
:data="treeCategoryList"
|
||||
style="width: 100%"
|
||||
stripe
|
||||
row-key="categoryId"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="defaultExpandAll"
|
||||
>
|
||||
<el-table-column prop="categoryName" label="分类名称" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="category-name-cell">
|
||||
<el-icon style="margin-right: 6px;">
|
||||
<Folder v-if="!row.children || row.children.length === 0" />
|
||||
<FolderOpened v-else />
|
||||
</el-icon>
|
||||
<span>{{ row.categoryName }}</span>
|
||||
<el-tag v-if="row.parentId === 0" size="small" type="info" class="root-tag">顶级</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="categoryDesc" label="描述" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<span class="category-desc">
|
||||
{{ row.categoryDesc || '暂无描述' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="create-time">{{ formatDate(row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleAddChild(row)"
|
||||
title="添加子分类"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加子分类
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="table-actions">
|
||||
<el-button link size="small" @click="toggleExpandAll">
|
||||
{{ defaultExpandAll ? '收起全部' : '展开全部' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-else
|
||||
:description="searchKeyword ? '未找到匹配的分类' : '暂无分类数据'"
|
||||
>
|
||||
<el-button type="primary" @click="handleAdd" v-if="!searchKeyword">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加第一个分类
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
@ -32,6 +142,8 @@
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
class="category-dialog"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
@ -39,70 +151,255 @@
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="父分类" prop="parentId">
|
||||
<el-cascader
|
||||
v-model="cascaderValue"
|
||||
:options="categoryTreeOptions"
|
||||
:props="cascaderProps"
|
||||
placeholder="请选择父分类(不选择则为顶级分类)"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleCascaderChange"
|
||||
/>
|
||||
<div class="form-tip">选择父分类可创建层级分类,留空则创建顶级分类</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="categoryName">
|
||||
<el-input
|
||||
v-model="formData.categoryName"
|
||||
placeholder="请输入分类名称"
|
||||
clearable
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类描述" prop="categoryDesc">
|
||||
<el-input
|
||||
v-model="formData.categoryDesc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getCategoryList, addCategory } from '@/api/knowledge'
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Folder, Refresh, Search, Edit, Delete, FolderOpened } from '@element-plus/icons-vue';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { getCategoryList, addCategory } from '@/api/knowledge.js';
|
||||
|
||||
const loading = ref(false)
|
||||
const categoryList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加分类')
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const categoryList = ref<any[]>([]);
|
||||
const searchKeyword = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('添加分类');
|
||||
const defaultExpandAll = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const cascaderValue = ref<any>(null);
|
||||
|
||||
const formData = reactive({
|
||||
// 级联选择器配置
|
||||
const cascaderProps = {
|
||||
value: 'categoryId',
|
||||
label: 'categoryName',
|
||||
children: 'children',
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
};
|
||||
|
||||
interface CategoryFormData {
|
||||
categoryId: number;
|
||||
parentId: number;
|
||||
categoryName: string;
|
||||
categoryDesc?: string;
|
||||
}
|
||||
|
||||
const formData = reactive<CategoryFormData>({
|
||||
categoryId: 0,
|
||||
parentId: 0,
|
||||
categoryName: '',
|
||||
})
|
||||
categoryDesc: '',
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
categoryName: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '分类名称长度应在 1 到 50 个字符之间', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
// 构建树形结构的分类列表
|
||||
const treeCategoryList = computed(() => {
|
||||
return buildCategoryTree(categoryList.value);
|
||||
});
|
||||
|
||||
// 用于表单选择的树形选项(排除当前编辑的分类)
|
||||
const categoryTreeOptions = computed(() => {
|
||||
if (formData.categoryId > 0) {
|
||||
// 编辑时,排除当前分类及其所有子分类
|
||||
const excludeIds = getDescendantIds(formData.categoryId, categoryList.value);
|
||||
return buildCategoryTree(
|
||||
categoryList.value.filter(cat => !excludeIds.includes(cat.categoryId))
|
||||
);
|
||||
}
|
||||
return buildCategoryTree(categoryList.value);
|
||||
});
|
||||
|
||||
// 获取某个分类的所有后代ID(包括自己)
|
||||
const getDescendantIds = (categoryId: number, list: any[]): number[] => {
|
||||
const ids = [categoryId];
|
||||
const children = list.filter(cat => cat.parentId === categoryId);
|
||||
children.forEach(child => {
|
||||
ids.push(...getDescendantIds(child.categoryId, list));
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 级联选择器变化处理
|
||||
const handleCascaderChange = (value: any) => {
|
||||
formData.parentId = value || 0;
|
||||
};
|
||||
|
||||
// 构建分类树
|
||||
const buildCategoryTree = (list: any[]): any[] => {
|
||||
const map = new Map();
|
||||
const roots: any[] = [];
|
||||
|
||||
// 首先创建所有节点的映射
|
||||
list.forEach(item => {
|
||||
map.set(item.categoryId, {
|
||||
...item,
|
||||
children: [],
|
||||
hasChildren: false,
|
||||
});
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
list.forEach(item => {
|
||||
const node = map.get(item.categoryId);
|
||||
const parentId = item.parentId || 0;
|
||||
|
||||
if (parentId === 0 || !map.has(parentId)) {
|
||||
// 根节点
|
||||
roots.push(node);
|
||||
} else {
|
||||
// 子节点
|
||||
const parent = map.get(parentId);
|
||||
parent.children.push(node);
|
||||
parent.hasChildren = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有搜索关键词,只显示匹配的节点及其父节点和子节点
|
||||
if (searchKeyword.value.trim()) {
|
||||
return filterTreeWithKeyword(roots, searchKeyword.value.toLowerCase());
|
||||
}
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
// 过滤树(保留匹配的节点及其父节点和子节点)
|
||||
const filterTreeWithKeyword = (tree: any[], keyword: string): any[] => {
|
||||
return tree
|
||||
.map(node => {
|
||||
const matched =
|
||||
node.categoryName?.toLowerCase().includes(keyword) ||
|
||||
node.categoryDesc?.toLowerCase().includes(keyword);
|
||||
|
||||
const filteredChildren = node.children ? filterTreeWithKeyword(node.children, keyword) : [];
|
||||
|
||||
if (matched || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren,
|
||||
hasChildren: filteredChildren.length > 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as any[];
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string | undefined | null): string => {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return String(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
};
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getCategoryList()
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || []
|
||||
categoryList.value = Array.isArray(data) ? data : []
|
||||
const res = await getCategoryList();
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || [];
|
||||
categoryList.value = Array.isArray(data) ? data : [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '获取分类列表失败')
|
||||
ElMessage.error(e.message || '获取分类列表失败');
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加分类'
|
||||
formData.categoryId = 0
|
||||
formData.categoryName = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
dialogTitle.value = '添加分类';
|
||||
// 先重置表单
|
||||
formRef.value?.resetFields();
|
||||
// 然后明确设置所有字段的值
|
||||
formData.categoryId = 0;
|
||||
formData.parentId = 0;
|
||||
formData.categoryName = '';
|
||||
formData.categoryDesc = '';
|
||||
cascaderValue.value = null;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleAddChild = (parent: any) => {
|
||||
dialogTitle.value = '添加子分类';
|
||||
// 先重置表单
|
||||
formRef.value?.resetFields();
|
||||
// 然后明确设置所有字段的值
|
||||
formData.categoryId = 0; // 确保是0,表示新增
|
||||
formData.parentId = parent.categoryId;
|
||||
formData.categoryName = '';
|
||||
formData.categoryDesc = '';
|
||||
cascaderValue.value = parent.categoryId;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
dialogTitle.value = '编辑分类'
|
||||
formData.categoryId = row.categoryId
|
||||
formData.categoryName = row.categoryName
|
||||
dialogVisible.value = true
|
||||
}
|
||||
dialogTitle.value = '编辑分类';
|
||||
formData.categoryId = row.categoryId;
|
||||
formData.parentId = row.parentId || 0;
|
||||
formData.categoryName = row.categoryName || '';
|
||||
formData.categoryDesc = row.categoryDesc || '';
|
||||
cascaderValue.value = row.parentId || null;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确认删除该分类?', '提示', {
|
||||
@ -112,62 +409,141 @@ const handleDelete = (row: any) => {
|
||||
}).then(async () => {
|
||||
try {
|
||||
// TODO: 实现删除API
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
ElMessage.success('删除成功');
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
ElMessage.error(e.message || '删除失败');
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
await addCategory({
|
||||
categoryName: formData.categoryName,
|
||||
})
|
||||
ElMessage.success('操作成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
if (!valid) return;
|
||||
|
||||
// 验证不能将自己设为父分类
|
||||
if (formData.categoryId > 0 && formData.parentId === formData.categoryId) {
|
||||
ElMessage.warning('不能将自己设为父分类');
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
// 明确判断是编辑还是添加
|
||||
const isEdit = formData.categoryId > 0;
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: any = {
|
||||
categoryName: formData.categoryName,
|
||||
};
|
||||
|
||||
if (formData.categoryDesc) {
|
||||
submitData.categoryDesc = formData.categoryDesc;
|
||||
}
|
||||
|
||||
if (formData.parentId > 0) {
|
||||
submitData.parentId = formData.parentId;
|
||||
}
|
||||
|
||||
// 只有明确是编辑时才传递categoryId
|
||||
// 添加时绝对不能包含categoryId字段
|
||||
if (isEdit) {
|
||||
submitData.categoryId = formData.categoryId;
|
||||
}
|
||||
// 注意:添加时submitData中不包含categoryId,后端会自动生成
|
||||
|
||||
await addCategory(submitData);
|
||||
ElMessage.success(isEdit ? '分类更新成功' : '分类添加成功');
|
||||
dialogVisible.value = false;
|
||||
formData.categoryId = 0;
|
||||
formData.parentId = 0;
|
||||
formData.categoryName = '';
|
||||
formData.categoryDesc = '';
|
||||
cascaderValue.value = null;
|
||||
formRef.value?.resetFields();
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 展开/收起全部
|
||||
const toggleExpandAll = () => {
|
||||
defaultExpandAll.value = !defaultExpandAll.value;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
fetchList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.category-manage {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
.header-left {
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-content {
|
||||
:deep(.el-card) {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
.category-name-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.root-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="knowledge-home">
|
||||
<!-- 如果当前是子路由(category 或 tag),显示子路由内容 -->
|
||||
<router-view v-if="isSubRoute" />
|
||||
|
||||
<!-- 否则显示知识库列表 -->
|
||||
<template v-else>
|
||||
<!-- 顶部搜索区域 -->
|
||||
<div class="search-section">
|
||||
<div class="search-content">
|
||||
@ -268,12 +273,13 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search, Plus, Folder, PriceTag, User, View, Star, Clock } from "@element-plus/icons-vue";
|
||||
|
||||
@ -310,6 +316,13 @@ interface StatItem {
|
||||
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查当前路由是否是子路由(category 或 tag)
|
||||
const isSubRoute = computed(() => {
|
||||
const path = route.path;
|
||||
return path.includes('/category') || path.includes('/tag');
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
const keyword = ref("");
|
||||
@ -422,7 +435,7 @@ function handleCategory() {
|
||||
}
|
||||
|
||||
function handleTags() {
|
||||
router.push(`/apps/knowledge/tags`);
|
||||
router.push(`/apps/knowledge/tag`);
|
||||
}
|
||||
|
||||
function handleDelete(repo: Knowledge) {
|
||||
|
||||
@ -1,29 +1,124 @@
|
||||
<template>
|
||||
<div class="tag-manage">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">标签管理</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加标签
|
||||
</el-button>
|
||||
<div class="header-left">
|
||||
<h2 class="page-title">
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
标签管理
|
||||
</h2>
|
||||
<p class="page-desc">管理知识库的标签,方便标记和检索内容</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加标签
|
||||
</el-button>
|
||||
<el-button @click="fetchList" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-cards">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="标签总数">
|
||||
<el-tag type="primary">{{ tagList.length }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索标签名称"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="max-width: 300px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 标签内容 -->
|
||||
<div class="tag-content">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="tagList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="tagId" label="ID" width="80" />
|
||||
<el-table-column prop="tagName" label="标签名称" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-card shadow="never" v-loading="loading">
|
||||
<!-- 表格视图 -->
|
||||
<div v-if="filteredTagList.length > 0" class="table-container">
|
||||
<el-table
|
||||
:data="filteredTagList"
|
||||
style="width: 100%"
|
||||
stripe
|
||||
:default-sort="{ prop: 'tagId', order: 'ascending' }"
|
||||
>
|
||||
<el-table-column prop="tagName" label="标签名称" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="tag-name-cell">
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
<el-tag
|
||||
:style="{
|
||||
backgroundColor: row.tagBackground || '#f0f0f0',
|
||||
color: row.tagColor || '#333',
|
||||
borderColor: row.tagBackground || '#e0e0e0'
|
||||
}"
|
||||
size="small"
|
||||
class="custom-tag"
|
||||
>
|
||||
{{ row.tagName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="usageCount" label="使用次数" width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.usageCount || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-else
|
||||
:description="searchKeyword ? '未找到匹配的标签' : '暂无标签数据'"
|
||||
>
|
||||
<el-button type="primary" @click="handleAdd" v-if="!searchKeyword">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加第一个标签
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
@ -32,6 +127,7 @@
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
@ -43,67 +139,148 @@
|
||||
<el-input
|
||||
v-model="formData.tagName"
|
||||
placeholder="请输入标签名称"
|
||||
clearable
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字体颜色" prop="tagColor">
|
||||
<el-color-picker
|
||||
v-model="formData.tagColor"
|
||||
:predefine="predefineColors"
|
||||
show-alpha
|
||||
/>
|
||||
<div class="form-tip">选择标签字体颜色(可选)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" prop="tagBackground">
|
||||
<el-color-picker
|
||||
v-model="formData.tagBackground"
|
||||
:predefine="predefineColors"
|
||||
show-alpha
|
||||
/>
|
||||
<div class="form-tip">选择标签背景颜色(可选)</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getTagList, addTag } from '@/api/knowledge'
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, PriceTag, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { getTagList, addTag } from '@/api/knowledge.js';
|
||||
|
||||
const loading = ref(false)
|
||||
const tagList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加标签')
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const tagList = ref<any[]>([]);
|
||||
const searchKeyword = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('添加标签');
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const formData = reactive({
|
||||
interface TagFormData {
|
||||
tagId: number;
|
||||
tagName: string;
|
||||
tagColor?: string;
|
||||
tagBackground?: string;
|
||||
}
|
||||
|
||||
const formData = reactive<TagFormData>({
|
||||
tagId: 0,
|
||||
tagName: '',
|
||||
})
|
||||
tagColor: '',
|
||||
tagBackground: '',
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
tagName: [
|
||||
{ required: true, message: '请输入标签名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '标签名称长度应在 1 到 50 个字符之间', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
// 预定义颜色
|
||||
const predefineColors = [
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#ff1493',
|
||||
];
|
||||
|
||||
// 过滤后的标签列表
|
||||
const filteredTagList = computed(() => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
return tagList.value;
|
||||
}
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
return tagList.value.filter(tag =>
|
||||
tag.tagName?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string | undefined | null): string => {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return String(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
};
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTagList()
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || []
|
||||
tagList.value = Array.isArray(data) ? data : []
|
||||
const res = await getTagList();
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || [];
|
||||
tagList.value = Array.isArray(data) ? data : [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '获取标签列表失败')
|
||||
ElMessage.error(e.message || '获取标签列表失败');
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加标签'
|
||||
formData.tagId = 0
|
||||
formData.tagName = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
dialogTitle.value = '添加标签';
|
||||
formRef.value?.resetFields();
|
||||
formData.tagId = 0;
|
||||
formData.tagName = '';
|
||||
formData.tagColor = '';
|
||||
formData.tagBackground = '';
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
dialogTitle.value = '编辑标签'
|
||||
formData.tagId = row.tagId
|
||||
formData.tagName = row.tagName
|
||||
dialogVisible.value = true
|
||||
}
|
||||
dialogTitle.value = '编辑标签';
|
||||
formData.tagId = row.tagId;
|
||||
formData.tagName = row.tagName || '';
|
||||
formData.tagColor = row.tagColor || '';
|
||||
formData.tagBackground = row.tagBackground || '';
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确认删除该标签?', '提示', {
|
||||
@ -113,63 +290,112 @@ const handleDelete = (row: any) => {
|
||||
}).then(async () => {
|
||||
try {
|
||||
// TODO: 实现删除API
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
ElMessage.success('删除成功');
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
ElMessage.error(e.message || '删除失败');
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
if (!valid) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await addTag({
|
||||
const isEdit = formData.tagId > 0;
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: any = {
|
||||
tagName: formData.tagName,
|
||||
})
|
||||
ElMessage.success('操作成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
// 始终传递颜色字段,即使用户没有选择也传递空字符串
|
||||
tagColor: formData.tagColor || '',
|
||||
tagBackground: formData.tagBackground || '',
|
||||
};
|
||||
|
||||
// 只有编辑时才传递tagId
|
||||
if (isEdit) {
|
||||
submitData.tagId = formData.tagId;
|
||||
}
|
||||
|
||||
await addTag(submitData);
|
||||
ElMessage.success(isEdit ? '标签更新成功' : '标签添加成功');
|
||||
dialogVisible.value = false;
|
||||
formData.tagId = 0;
|
||||
formData.tagName = '';
|
||||
formData.tagColor = '';
|
||||
formData.tagBackground = '';
|
||||
formRef.value?.resetFields();
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
ElMessage.error(e.message || '操作失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
fetchList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-manage {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
.header-left {
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-content {
|
||||
:deep(.el-card) {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
.tag-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.custom-tag {
|
||||
border: 1px solid;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
|
||||
// Settings 父路由组件,用于显示子路由
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Settings 父路由容器 */
|
||||
</style>
|
||||
|
||||
@ -1,16 +1,420 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>菜单管理</h2>
|
||||
<el-button type="primary" @click="handleAddMenu = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
<div class="system-info-container">
|
||||
<el-card class="info-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><InfoFilled /></el-icon>
|
||||
<span class="header-title">系统信息</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="loading" class="info-content">
|
||||
<!-- 系统基本信息 -->
|
||||
<el-descriptions title="系统基本信息" :column="2" border class="info-section">
|
||||
<el-descriptions-item label="系统名称">
|
||||
<el-tag type="primary">{{ systemInfo.name || '云泽管理系统' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="系统版本">
|
||||
<el-tag>{{ systemInfo.version || '1.0.0' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行环境">
|
||||
<el-tag type="success">{{ systemInfo.environment || 'Production' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="系统时间">
|
||||
{{ currentTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行时间">
|
||||
{{ systemInfo.uptime || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="时区">
|
||||
{{ systemInfo.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 服务器信息 -->
|
||||
<el-descriptions title="服务器信息" :column="2" border class="info-section">
|
||||
<el-descriptions-item label="服务器地址">
|
||||
{{ systemInfo.serverHost || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="API地址">
|
||||
{{ apiBaseUrl }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作系统">
|
||||
{{ systemInfo.os || clientInfo.os }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="CPU核心数">
|
||||
{{ systemInfo.cpuCores || clientInfo.cpuCores }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="内存使用">
|
||||
<el-progress
|
||||
:percentage="systemInfo.memoryUsage || 0"
|
||||
:color="getMemoryColor(systemInfo.memoryUsage)"
|
||||
:format="(percentage) => `${percentage}%`"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="磁盘使用">
|
||||
<el-progress
|
||||
:percentage="systemInfo.diskUsage || 0"
|
||||
:color="getDiskColor(systemInfo.diskUsage)"
|
||||
:format="(percentage) => `${percentage}%`"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 数据库信息 -->
|
||||
<el-descriptions title="数据库信息" :column="2" border class="info-section">
|
||||
<el-descriptions-item label="数据库类型">
|
||||
<el-tag type="info">{{ systemInfo.dbType || 'MySQL' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库版本">
|
||||
{{ systemInfo.dbVersion || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库状态">
|
||||
<el-tag :type="systemInfo.dbStatus === 'connected' ? 'success' : 'danger'">
|
||||
{{ systemInfo.dbStatus === 'connected' ? '已连接' : '未连接' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="连接数">
|
||||
{{ systemInfo.dbConnections || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 客户端信息 -->
|
||||
<el-descriptions title="客户端信息" :column="2" border class="info-section">
|
||||
<el-descriptions-item label="浏览器">
|
||||
{{ clientInfo.browser }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="浏览器版本">
|
||||
{{ clientInfo.browserVersion }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作系统">
|
||||
{{ clientInfo.os }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="屏幕分辨率">
|
||||
{{ clientInfo.screenResolution }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="设备类型">
|
||||
<el-tag :type="clientInfo.isMobile ? 'warning' : 'success'">
|
||||
{{ clientInfo.isMobile ? '移动设备' : '桌面设备' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户代理">
|
||||
<span class="user-agent">{{ clientInfo.userAgent }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 应用信息 -->
|
||||
<el-descriptions title="应用信息" :column="2" border class="info-section">
|
||||
<el-descriptions-item label="前端框架">
|
||||
<el-tag type="success">Vue 3</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="UI框架">
|
||||
<el-tag type="primary">Element Plus</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="构建工具">
|
||||
<el-tag type="info">Vite</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Node版本">
|
||||
{{ clientInfo.nodeVersion || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="refreshInfo" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新信息
|
||||
</el-button>
|
||||
<el-button @click="copySystemInfo">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
复制信息
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { InfoFilled, Refresh, DocumentCopy } from '@element-plus/icons-vue';
|
||||
|
||||
// 系统信息数据
|
||||
const loading = ref(false);
|
||||
const systemInfo = ref<any>({});
|
||||
const currentTime = ref(new Date().toLocaleString('zh-CN'));
|
||||
|
||||
// API基础地址
|
||||
const apiBaseUrl = computed(() => {
|
||||
return import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
});
|
||||
|
||||
// 客户端信息
|
||||
const clientInfo = computed(() => {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
// 检测浏览器
|
||||
let browser = 'Unknown';
|
||||
let browserVersion = '';
|
||||
if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
||||
browser = 'Chrome';
|
||||
const match = ua.match(/Chrome\/([\d.]+)/);
|
||||
browserVersion = match ? match[1] : '';
|
||||
} else if (ua.includes('Firefox')) {
|
||||
browser = 'Firefox';
|
||||
const match = ua.match(/Firefox\/([\d.]+)/);
|
||||
browserVersion = match ? match[1] : '';
|
||||
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
||||
browser = 'Safari';
|
||||
const match = ua.match(/Version\/([\d.]+)/);
|
||||
browserVersion = match ? match[1] : '';
|
||||
} else if (ua.includes('Edg')) {
|
||||
browser = 'Edge';
|
||||
const match = ua.match(/Edg\/([\d.]+)/);
|
||||
browserVersion = match ? match[1] : '';
|
||||
}
|
||||
|
||||
// 检测操作系统
|
||||
let os = 'Unknown';
|
||||
if (ua.includes('Win')) os = 'Windows';
|
||||
else if (ua.includes('Mac')) os = 'macOS';
|
||||
else if (ua.includes('Linux')) os = 'Linux';
|
||||
else if (ua.includes('Android')) os = 'Android';
|
||||
else if (ua.includes('iOS')) os = 'iOS';
|
||||
|
||||
// 检测是否为移动设备
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
||||
|
||||
return {
|
||||
browser,
|
||||
browserVersion,
|
||||
os,
|
||||
screenResolution: `${screen.width}x${screen.height}`,
|
||||
isMobile,
|
||||
userAgent: ua,
|
||||
cpuCores: navigator.hardwareConcurrency || '-',
|
||||
nodeVersion: '-', // Node.js 版本在浏览器环境中不可用
|
||||
};
|
||||
});
|
||||
|
||||
// 获取内存使用颜色
|
||||
const getMemoryColor = (usage: number | undefined) => {
|
||||
if (!usage) return '#409eff';
|
||||
if (usage >= 90) return '#f56c6c';
|
||||
if (usage >= 70) return '#e6a23c';
|
||||
return '#67c23a';
|
||||
};
|
||||
|
||||
// 获取磁盘使用颜色
|
||||
const getDiskColor = (usage: number | undefined) => {
|
||||
if (!usage) return '#409eff';
|
||||
if (usage >= 90) return '#f56c6c';
|
||||
if (usage >= 70) return '#e6a23c';
|
||||
return '#67c23a';
|
||||
};
|
||||
|
||||
// 获取系统信息
|
||||
async function fetchSystemInfo() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 如果有后端API,可以在这里调用
|
||||
// const res = await getSystemInfo();
|
||||
// if (res.success && res.data) {
|
||||
// systemInfo.value = res.data;
|
||||
// }
|
||||
|
||||
// 模拟数据(如果有API会替换)
|
||||
systemInfo.value = {
|
||||
name: '云泽管理系统',
|
||||
version: '1.0.0',
|
||||
environment: 'Production',
|
||||
uptime: '24天 5小时 30分钟',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
serverHost: window.location.hostname,
|
||||
os: 'Linux',
|
||||
cpuCores: '4',
|
||||
memoryUsage: 65,
|
||||
diskUsage: 42,
|
||||
dbType: 'MySQL',
|
||||
dbVersion: '8.0',
|
||||
dbStatus: 'connected',
|
||||
dbConnections: 15,
|
||||
};
|
||||
} catch (err: any) {
|
||||
ElMessage.error('获取系统信息失败:' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新信息
|
||||
function refreshInfo() {
|
||||
fetchSystemInfo();
|
||||
currentTime.value = new Date().toLocaleString('zh-CN');
|
||||
ElMessage.success('信息已刷新');
|
||||
}
|
||||
|
||||
// 复制系统信息
|
||||
function copySystemInfo() {
|
||||
const infoText = `系统信息报告
|
||||
==================
|
||||
系统名称:${systemInfo.value.name || '-'}
|
||||
系统版本:${systemInfo.value.version || '-'}
|
||||
运行环境:${systemInfo.value.environment || '-'}
|
||||
系统时间:${currentTime.value}
|
||||
运行时间:${systemInfo.value.uptime || '-'}
|
||||
|
||||
服务器信息:
|
||||
- 服务器地址:${systemInfo.value.serverHost || '-'}
|
||||
- API地址:${apiBaseUrl.value}
|
||||
- 操作系统:${systemInfo.value.os || '-'}
|
||||
- CPU核心数:${systemInfo.value.cpuCores || '-'}
|
||||
- 内存使用:${systemInfo.value.memoryUsage || 0}%
|
||||
- 磁盘使用:${systemInfo.value.diskUsage || 0}%
|
||||
|
||||
数据库信息:
|
||||
- 数据库类型:${systemInfo.value.dbType || '-'}
|
||||
- 数据库版本:${systemInfo.value.dbVersion || '-'}
|
||||
- 数据库状态:${systemInfo.value.dbStatus === 'connected' ? '已连接' : '未连接'}
|
||||
- 连接数:${systemInfo.value.dbConnections || '-'}
|
||||
|
||||
客户端信息:
|
||||
- 浏览器:${clientInfo.value.browser} ${clientInfo.value.browserVersion}
|
||||
- 操作系统:${clientInfo.value.os}
|
||||
- 屏幕分辨率:${clientInfo.value.screenResolution}
|
||||
- 设备类型:${clientInfo.value.isMobile ? '移动设备' : '桌面设备'}
|
||||
|
||||
生成时间:${new Date().toLocaleString('zh-CN')}`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(infoText).then(() => {
|
||||
ElMessage.success('系统信息已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
});
|
||||
} else {
|
||||
// 降级方案
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = infoText;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
ElMessage.success('系统信息已复制到剪贴板');
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
let timeInterval: NodeJS.Timeout | null = null;
|
||||
onMounted(() => {
|
||||
fetchSystemInfo();
|
||||
// 每秒更新时间
|
||||
timeInterval = setInterval(() => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.system-info-container {
|
||||
padding: 24px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
min-height: 100%;
|
||||
|
||||
.info-card {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.header-icon {
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
:deep(.el-descriptions__title) {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #4f84ff;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.user-agent {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.system-info-container {
|
||||
padding: 12px;
|
||||
|
||||
.info-card {
|
||||
.info-content {
|
||||
.info-section {
|
||||
:deep(.el-descriptions) {
|
||||
:deep(.el-descriptions__table) {
|
||||
.el-descriptions__cell {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -120,7 +120,7 @@
|
||||
<!-- 文件数据列表 -->
|
||||
<div class="file-list stylish-panel">
|
||||
<el-table
|
||||
:data="filteredFiles"
|
||||
:data="paginatedFiles"
|
||||
v-loading="loading"
|
||||
highlight-current-row
|
||||
border
|
||||
@ -361,9 +361,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Download,
|
||||
Delete,
|
||||
@ -373,58 +373,16 @@ import {
|
||||
Link,
|
||||
UploadFilled,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { getAllFiles, getMyFiles, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const recentFiles = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: "项目报告.pdf",
|
||||
size: 2456789,
|
||||
type: "PDF",
|
||||
uploadTime: "2024-01-15 10:30:25",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "数据统计.xlsx",
|
||||
size: 123456,
|
||||
type: "Excel",
|
||||
uploadTime: "2024-01-14 14:20:10",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "产品图片.jpg",
|
||||
size: 345678,
|
||||
type: "Image",
|
||||
uploadTime: "2024-01-13 09:15:45",
|
||||
},
|
||||
]);
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
PDF: "fa-solid fa-file-pdf",
|
||||
Excel: "fa-solid fa-file-excel",
|
||||
Word: "fa-solid fa-file-word",
|
||||
Image: "fa-solid fa-file-image",
|
||||
Video: "fa-solid fa-file-video",
|
||||
Audio: "fa-solid fa-file-audio",
|
||||
Archive: "fa-solid fa-file-archive",
|
||||
};
|
||||
return iconMap[type] || "fa-solid fa-file";
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// mock 下列数据和函数,由于原代码没提供全部上下文
|
||||
const categories = ref(["文档", "图片", "视频", "音频", "其他"]);
|
||||
const filteredFiles = ref([]);
|
||||
// 文件列表数据
|
||||
const allFiles = ref<any[]>([]);
|
||||
const categories = ref<string[]>([]);
|
||||
const filteredFiles = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const searchKeyword = ref("");
|
||||
const filterCategory = ref("");
|
||||
const showMyFiles = ref(false);
|
||||
@ -433,6 +391,106 @@ const currentPage = ref(1);
|
||||
const totalFiles = ref(0);
|
||||
const showUploadDialog = ref(false);
|
||||
|
||||
// 获取文件列表
|
||||
async function fetchFiles() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
let res;
|
||||
if (showMyFiles.value) {
|
||||
res = await getMyFiles();
|
||||
} else {
|
||||
res = await getAllFiles();
|
||||
}
|
||||
|
||||
if (res.success && res.data) {
|
||||
const files = Array.isArray(res.data) ? res.data : [];
|
||||
allFiles.value = files.map((file: any) => ({
|
||||
...file,
|
||||
upload_time: file.upload_time || file.create_time,
|
||||
}));
|
||||
|
||||
// 提取所有分类
|
||||
const uniqueCategories = new Set<string>();
|
||||
files.forEach((file: any) => {
|
||||
if (file.category) {
|
||||
uniqueCategories.add(file.category);
|
||||
}
|
||||
});
|
||||
categories.value = Array.from(uniqueCategories).sort();
|
||||
|
||||
applyFilters();
|
||||
totalFiles.value = filteredFiles.value.length;
|
||||
} else {
|
||||
error.value = res.message || "获取文件列表失败";
|
||||
allFiles.value = [];
|
||||
totalFiles.value = 0;
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取文件列表失败";
|
||||
allFiles.value = [];
|
||||
totalFiles.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用搜索和筛选
|
||||
function applyFilters() {
|
||||
let result = [...allFiles.value];
|
||||
|
||||
// 分类筛选
|
||||
if (filterCategory.value) {
|
||||
result = result.filter((file) => file.category === filterCategory.value);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
result = result.filter(
|
||||
(file) =>
|
||||
(file.file_name && file.file_name.toLowerCase().includes(keyword)) ||
|
||||
(file.original_name && file.original_name.toLowerCase().includes(keyword)) ||
|
||||
(file.category && file.category.toLowerCase().includes(keyword))
|
||||
);
|
||||
}
|
||||
|
||||
filteredFiles.value = result;
|
||||
totalFiles.value = result.length;
|
||||
|
||||
// 重置到第一页
|
||||
if (currentPage.value > 1 && result.length === 0) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number | string | undefined | null): string => {
|
||||
if (!bytes && bytes !== 0) return "0 B";
|
||||
const bytesValue = typeof bytes === 'string' ? parseFloat(bytes) : Number(bytes);
|
||||
if (isNaN(bytesValue) || bytesValue < 0) return "0 B";
|
||||
if (bytesValue === 0) return "0 B";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytesValue) / Math.log(k));
|
||||
return parseFloat((bytesValue / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string | number | undefined | null): string => {
|
||||
if (!dateStr) return "-";
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return String(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL ;
|
||||
@ -453,28 +511,53 @@ const uploadForm = ref({
|
||||
const showFileDetail = ref(false);
|
||||
const currentFile = ref<any>(null);
|
||||
|
||||
const handleSearch = () => {};
|
||||
const handleFilter = () => {};
|
||||
const handleSizeChange = () => {};
|
||||
const handleCurrentChange = () => {};
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
applyFilters();
|
||||
};
|
||||
|
||||
// 筛选处理
|
||||
const handleFilter = () => {
|
||||
fetchFiles();
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1;
|
||||
};
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
};
|
||||
|
||||
// 上传对话框关闭
|
||||
const handleUploadClose = () => {
|
||||
showUploadDialog.value = false;
|
||||
uploadForm.value.category = "";
|
||||
uploadForm.value.isPublic = false;
|
||||
};
|
||||
|
||||
// 上传成功
|
||||
const handleUploadSuccess = (response: any, file: any) => {
|
||||
ElMessage.success('文件上传成功!');
|
||||
// 关闭对话框
|
||||
showUploadDialog.value = false;
|
||||
// 可以刷新文件列表
|
||||
// loadFiles();
|
||||
if (response.success) {
|
||||
ElMessage.success('文件上传成功!');
|
||||
showUploadDialog.value = false;
|
||||
uploadForm.value.category = "";
|
||||
uploadForm.value.isPublic = false;
|
||||
fetchFiles();
|
||||
} else {
|
||||
ElMessage.error(response.message || '文件上传失败!');
|
||||
}
|
||||
};
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = (error: Error, file: any) => {
|
||||
ElMessage.error('文件上传失败:' + error.message);
|
||||
ElMessage.error('文件上传失败:' + (error.message || error));
|
||||
};
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = (file: File) => {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
@ -484,23 +567,143 @@ const beforeUpload = (file: File) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// 提交上传
|
||||
const submitUpload = () => {
|
||||
// Element Plus 的 el-upload 组件会自动处理上传
|
||||
ElMessage.info('开始上传文件...');
|
||||
// 不需要额外处理
|
||||
};
|
||||
const viewFile = (row: any) => {};
|
||||
const formatDate = (val: string | number) => val;
|
||||
const copyFileUrl = (file: any) => {};
|
||||
|
||||
// 查看文件详情
|
||||
const viewFile = async (row: any) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getFileById(row.id);
|
||||
if (res.success && res.data) {
|
||||
currentFile.value = res.data;
|
||||
showFileDetail.value = true;
|
||||
} else {
|
||||
ElMessage.error(res.message || "获取文件详情失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "获取文件详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 复制文件链接
|
||||
const copyFileUrl = (file: any) => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
const fileUrl = `${baseUrl}/api/files/${file.id}/download`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(fileUrl).then(() => {
|
||||
ElMessage.success('文件链接已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
});
|
||||
} else {
|
||||
// 降级方案
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = fileUrl;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
ElMessage.success('文件链接已复制到剪贴板');
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: any) => {
|
||||
// 这里实现下载逻辑
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
const downloadUrl = `${baseUrl}/api/files/${file.id}/download`;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
if (token) {
|
||||
// 如果需要带token下载,可以使用fetch然后创建blob
|
||||
fetch(downloadUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.download = file.original_name || file.file_name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
ElMessage.success('文件下载开始');
|
||||
})
|
||||
.catch(err => {
|
||||
ElMessage.error('文件下载失败:' + err.message);
|
||||
});
|
||||
} else {
|
||||
link.download = file.original_name || file.file_name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = (file: any) => {
|
||||
// 这里实现删除逻辑
|
||||
// 删除文件
|
||||
const deleteFile = async (file: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除文件「${file.original_name || file.file_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
}
|
||||
);
|
||||
|
||||
loading.value = true;
|
||||
const res = await deleteFileApi(file.id);
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功');
|
||||
await fetchFiles();
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') {
|
||||
ElMessage.error(err.message || '删除失败');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([showMyFiles], () => {
|
||||
fetchFiles();
|
||||
});
|
||||
|
||||
// 分页计算
|
||||
const paginatedFiles = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value;
|
||||
const end = start + pageSize.value;
|
||||
return filteredFiles.value.slice(start, end);
|
||||
});
|
||||
|
||||
// 更新表格数据源为分页后的数据
|
||||
watch([filteredFiles, currentPage, pageSize], () => {
|
||||
// filteredFiles 已经在 computed 中处理,但表格需要直接使用 paginatedFiles
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
fetchFiles();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -290,12 +290,13 @@ func (c *KnowledgeController) GetTags() {
|
||||
var result []map[string]interface{}
|
||||
for _, tag := range tags {
|
||||
result = append(result, map[string]interface{}{
|
||||
"tagId": tag.TagId,
|
||||
"tagName": tag.TagName,
|
||||
"tagColor": tag.TagColor,
|
||||
"usageCount": tag.UsageCount,
|
||||
"createTime": tag.CreateTime,
|
||||
"updateTime": tag.UpdateTime,
|
||||
"tagId": tag.TagId,
|
||||
"tagName": tag.TagName,
|
||||
"tagColor": tag.TagColor,
|
||||
"tagBackground": tag.TagBackground,
|
||||
"usageCount": tag.UsageCount,
|
||||
"createTime": tag.CreateTime,
|
||||
"updateTime": tag.UpdateTime,
|
||||
})
|
||||
}
|
||||
|
||||
@ -322,7 +323,8 @@ func (c *KnowledgeController) AddCategory() {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := models.AddCategory(&category)
|
||||
// 不处理id,直接添加,表自动递增
|
||||
_, err = models.AddCategory(&category)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
@ -336,12 +338,12 @@ func (c *KnowledgeController) AddCategory() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "添加成功",
|
||||
"data": map[string]interface{}{"categoryId": id},
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTag 添加标签
|
||||
// AddTag 添加或更新标签
|
||||
// @router /api/knowledge/tag/add [post]
|
||||
func (c *KnowledgeController) AddTag() {
|
||||
var tag models.KnowledgeTag
|
||||
@ -356,6 +358,31 @@ func (c *KnowledgeController) AddTag() {
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是添加还是更新
|
||||
if tag.TagId > 0 {
|
||||
// 更新操作
|
||||
err = models.UpdateTag(&tag)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "更新标签失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "更新成功",
|
||||
"data": map[string]interface{}{"tagId": tag.TagId},
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 添加操作:确保 TagId 为 0,让数据库自动生成
|
||||
tag.TagId = 0
|
||||
id, err := models.AddTag(&tag)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
|
||||
65
server/database/add_knowledge_menus.sql
Normal file
65
server/database/add_knowledge_menus.sql
Normal file
@ -0,0 +1,65 @@
|
||||
-- 添加知识库管理相关的菜单项
|
||||
-- 注意:需要先查询知识库菜单的ID(parent_id),假设为 11
|
||||
|
||||
-- 如果知识库菜单ID为11,添加分类管理和标签管理菜单
|
||||
-- 请根据实际数据库中的知识库菜单ID修改下面的 parent_id 值
|
||||
|
||||
-- 查询知识库菜单ID(如果需要)
|
||||
-- SELECT id FROM yz_menus WHERE path = '/apps/knowledge';
|
||||
|
||||
-- 添加分类管理菜单(如果不存在)
|
||||
INSERT INTO yz_menus (
|
||||
name,
|
||||
path,
|
||||
parent_id,
|
||||
icon,
|
||||
`order`,
|
||||
status,
|
||||
component_path,
|
||||
menu_type,
|
||||
description
|
||||
)
|
||||
SELECT
|
||||
'分类管理',
|
||||
'/apps/knowledge/category',
|
||||
id,
|
||||
'fa-solid fa-folder',
|
||||
2,
|
||||
1,
|
||||
'@/views/apps/knowledge/category/index.vue',
|
||||
1,
|
||||
'知识库分类管理'
|
||||
FROM yz_menus
|
||||
WHERE path = '/apps/knowledge'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM yz_menus WHERE path = '/apps/knowledge/category'
|
||||
);
|
||||
|
||||
-- 添加标签管理菜单(如果不存在)
|
||||
INSERT INTO yz_menus (
|
||||
name,
|
||||
path,
|
||||
parent_id,
|
||||
icon,
|
||||
`order`,
|
||||
status,
|
||||
component_path,
|
||||
menu_type,
|
||||
description
|
||||
)
|
||||
SELECT
|
||||
'标签管理',
|
||||
'/apps/knowledge/tag',
|
||||
id,
|
||||
'fa-solid fa-tags',
|
||||
3,
|
||||
1,
|
||||
'@/views/apps/knowledge/tag/index.vue',
|
||||
1,
|
||||
'知识库标签管理'
|
||||
FROM yz_menus
|
||||
WHERE path = '/apps/knowledge'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM yz_menus WHERE path = '/apps/knowledge/tag'
|
||||
);
|
||||
|
||||
@ -54,12 +54,13 @@ func (kc *KnowledgeCategory) TableName() string {
|
||||
|
||||
// KnowledgeTag 知识库标签模型
|
||||
type KnowledgeTag struct {
|
||||
TagId int `orm:"column(tag_id);pk;auto" json:"tagId"`
|
||||
TagName string `orm:"column(tag_name);size(50);unique" json:"tagName"`
|
||||
TagColor string `orm:"column(tag_color);size(20);null" json:"tagColor"`
|
||||
UsageCount int `orm:"column(usage_count);default(0)" json:"usageCount"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"updateTime"`
|
||||
TagId int `orm:"column(tag_id);pk;auto" json:"tagId"`
|
||||
TagName string `orm:"column(tag_name);size(50);unique" json:"tagName"`
|
||||
TagColor string `orm:"column(tag_color);size(20);null" json:"tagColor"`
|
||||
TagBackground string `orm:"column(tag_background);size(20);null" json:"tagBackground"`
|
||||
UsageCount int `orm:"column(usage_count);default(0)" json:"usageCount"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"updateTime"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
@ -286,9 +287,23 @@ func AddCategory(category *KnowledgeCategory) (int64, error) {
|
||||
return id, err
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func UpdateCategory(category *KnowledgeCategory) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Update(category, "CategoryName", "CategoryDesc", "ParentId", "SortOrder")
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTag 添加标签
|
||||
func AddTag(tag *KnowledgeTag) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
id, err := o.Insert(tag)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
func UpdateTag(tag *KnowledgeTag) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Update(tag, "TagName", "TagColor", "TagBackground")
|
||||
return err
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user