serv\web-侧边栏 标签统一
This commit is contained in:
360
schoolNewsWeb/TAG_SYSTEM_MIGRATION.md
Normal file
360
schoolNewsWeb/TAG_SYSTEM_MIGRATION.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 前端标签系统类型化改造文档
|
||||
|
||||
## 📝 改造概述
|
||||
|
||||
前端标签系统已更新,支持3种标签类型,与后端保持一致:
|
||||
1. **文章分类标签** (tagType=1) - 替代原 `ResourceCategory`
|
||||
2. **课程分类标签** (tagType=2)
|
||||
3. **学习任务分类标签** (tagType=3)
|
||||
|
||||
### 改造日期
|
||||
2025-10-27
|
||||
|
||||
---
|
||||
|
||||
## 🔄 前端变更内容
|
||||
|
||||
### 1. 类型定义更新
|
||||
|
||||
#### 文件:`src/types/resource/index.ts`
|
||||
|
||||
**新增 TagType 枚举:**
|
||||
```typescript
|
||||
/**
|
||||
* 标签类型枚举
|
||||
*/
|
||||
export enum TagType {
|
||||
/** 文章分类标签 */
|
||||
ARTICLE_CATEGORY = 1,
|
||||
/** 课程分类标签 */
|
||||
COURSE_CATEGORY = 2,
|
||||
/** 学习任务分类标签 */
|
||||
LEARNING_TASK_CATEGORY = 3
|
||||
}
|
||||
```
|
||||
|
||||
**Tag 接口更新:**
|
||||
```typescript
|
||||
export interface Tag extends BaseDTO {
|
||||
/** 标签ID */
|
||||
tagID?: string;
|
||||
/** 标签名称 */
|
||||
name?: string;
|
||||
/** 标签描述 */
|
||||
description?: string;
|
||||
/** 标签颜色 */
|
||||
color?: string;
|
||||
/** 标签类型(1-文章分类标签 2-课程分类标签 3-学习任务分类标签) */
|
||||
tagType?: number; // ✅ 新增字段
|
||||
/** 排序号 */
|
||||
orderNum?: number;
|
||||
/** 状态(0禁用 1启用) */
|
||||
status?: number;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
/** 更新者 */
|
||||
updater?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**ResourceVO 接口更新:**
|
||||
```typescript
|
||||
/**
|
||||
* 资源视图对象(已废弃category字段)
|
||||
* @deprecated category字段已废弃,请使用tags字段中tagType=1的标签
|
||||
*/
|
||||
export interface ResourceVO extends BaseDTO{
|
||||
resource: Resource;
|
||||
/** @deprecated 已废弃,改用tags字段(tagType=1表示文章分类标签) */
|
||||
category?: ResourceCategory;
|
||||
tags: Tag[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. API 接口更新
|
||||
|
||||
#### 文件:`src/apis/resource/resourceTag.ts`
|
||||
|
||||
**新增方法:**
|
||||
```typescript
|
||||
/**
|
||||
* 根据标签类型获取标签列表
|
||||
* @param tagType 标签类型(1-文章分类标签 2-课程分类标签 3-学习任务分类标签)
|
||||
* @returns Promise<ResultDomain<Tag>>
|
||||
*/
|
||||
async getTagsByType(tagType: number): Promise<ResultDomain<Tag>> {
|
||||
const response = await api.get<Tag>(`/news/tags/type/${tagType}`);
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
```typescript
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import { TagType } from '@/types/resource';
|
||||
|
||||
// 获取文章分类标签
|
||||
const articleTags = await resourceTagApi.getTagsByType(TagType.ARTICLE_CATEGORY);
|
||||
|
||||
// 获取课程分类标签
|
||||
const courseTags = await resourceTagApi.getTagsByType(TagType.COURSE_CATEGORY);
|
||||
|
||||
// 获取学习任务分类标签
|
||||
const taskTags = await resourceTagApi.getTagsByType(TagType.LEARNING_TASK_CATEGORY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ResourceCategory API 废弃
|
||||
|
||||
#### 文件:`src/apis/resource/resourceCategory.ts`
|
||||
|
||||
**⚠️ 此 API 已废弃!**
|
||||
|
||||
从2025-10-27起,资源分类功能已迁移到标签系统(Tag)中。
|
||||
|
||||
**API 迁移对照表:**
|
||||
|
||||
| 原 ResourceCategory API | 新 Tag API | 说明 |
|
||||
|------------------------|-----------|------|
|
||||
| `resourceCategoryApi.getCategoryList()` | `resourceTagApi.getTagsByType(1)` | 获取文章分类标签列表 |
|
||||
| `resourceCategoryApi.getCategoryById(id)` | `resourceTagApi.getTagById(id)` | 获取标签详情 |
|
||||
| `resourceCategoryApi.createCategory(category)` | `resourceTagApi.createTag({...category, tagType: 1})` | 创建文章分类标签 |
|
||||
| `resourceCategoryApi.updateCategory(category)` | `resourceTagApi.updateTag(category)` | 更新标签 |
|
||||
| `resourceCategoryApi.deleteCategory(id)` | `resourceTagApi.deleteTag(id)` | 删除标签 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 已更新的组件
|
||||
|
||||
### 1. ArticleManagementView.vue
|
||||
**文件:** `src/views/admin/manage/resource/ArticleManagementView.vue`
|
||||
|
||||
**变更内容:**
|
||||
```typescript
|
||||
// 修改前
|
||||
import { resourceApi, resourceCategoryApi } from '@/apis/resource'
|
||||
const categoryList = ref<ResourceCategory[]>([]);
|
||||
const res = await resourceCategoryApi.getCategoryList();
|
||||
|
||||
// 修改后
|
||||
import { resourceApi, resourceTagApi } from '@/apis/resource'
|
||||
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. ArticleAddView.vue
|
||||
**文件:** `src/views/article/ArticleAddView.vue`
|
||||
|
||||
**变更内容:**
|
||||
```typescript
|
||||
// 修改前
|
||||
import { resourceCategoryApi, resourceTagApi, resourceApi } from '@/apis/resource';
|
||||
const categoryList = ref<ResourceCategory[]>([]);
|
||||
const result = await resourceCategoryApi.getCategoryList();
|
||||
|
||||
// 修改后
|
||||
import { resourceTagApi, resourceApi } from '@/apis/resource';
|
||||
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
const result = await resourceTagApi.getTagsByType(TagType.ARTICLE_CATEGORY);
|
||||
```
|
||||
|
||||
**模板更新:**
|
||||
```vue
|
||||
<!-- 修改前 -->
|
||||
<el-option
|
||||
v-for="category in categoryList"
|
||||
:key="category.tagID || category.id"
|
||||
:label="category.name"
|
||||
:value="category.tagID || category.id || ''"
|
||||
/>
|
||||
|
||||
<!-- 修改后 -->
|
||||
<el-option
|
||||
v-for="category in categoryList"
|
||||
:key="category.tagID || category.id"
|
||||
:label="category.name"
|
||||
:value="category.tagID || category.id || ''"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ResourceSideBar.vue
|
||||
**文件:** `src/views/resource-center/components/ResourceSideBar.vue`
|
||||
|
||||
**变更内容:**
|
||||
```typescript
|
||||
// 修改前
|
||||
import { resourceCategoryApi } from '@/apis/resource';
|
||||
import type { ResourceCategory } from '@/types/resource';
|
||||
const categories = ref<ResourceCategory[]>([]);
|
||||
const res = await resourceCategoryApi.getCategoryList();
|
||||
|
||||
// 修改后
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import type { Tag, TagType } from '@/types/resource';
|
||||
const categories = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
||||
```
|
||||
|
||||
**模板更新:**
|
||||
```vue
|
||||
<!-- 修改前 -->
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.tagID"
|
||||
:class="{ active: category.tagID === activeCategoryId }"
|
||||
>
|
||||
|
||||
<!-- 修改后 -->
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.tagID || category.id"
|
||||
:class="{ active: (category.tagID || category.id) === activeCategoryId }"
|
||||
>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 学习任务和课程标签说明
|
||||
|
||||
### 课程标签(tagType=2)
|
||||
|
||||
**类型定义:** `src/types/study/index.ts`
|
||||
|
||||
课程标签使用统一的标签系统(tb_tag表):
|
||||
- 使用 `tagType=2` 表示课程分类标签
|
||||
- 通过 `resourceTagApi.getTagsByType(TagType.COURSE_CATEGORY)` 获取课程分类标签
|
||||
|
||||
**使用示例:**
|
||||
```typescript
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import { TagType } from '@/types/resource';
|
||||
|
||||
// 在课程管理页面加载课程分类标签
|
||||
async function loadCourseTags() {
|
||||
const result = await resourceTagApi.getTagsByType(TagType.COURSE_CATEGORY);
|
||||
if (result.success) {
|
||||
courseTags.value = result.dataList || [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 学习任务标签(tagType=3)
|
||||
|
||||
**类型定义:** `src/types/study/index.ts`
|
||||
|
||||
学习任务分类使用统一的标签系统(tb_tag表):
|
||||
- 使用 `tagType=3` 表示学习任务分类标签
|
||||
- 通过 `resourceTagApi.getTagsByType(TagType.LEARNING_TASK_CATEGORY)` 获取学习任务分类标签
|
||||
|
||||
**使用示例:**
|
||||
```typescript
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import { TagType } from '@/types/resource';
|
||||
|
||||
// 在学习任务管理页面加载任务分类标签
|
||||
async function loadTaskTags() {
|
||||
const result = await resourceTagApi.getTagsByType(TagType.LEARNING_TASK_CATEGORY);
|
||||
if (result.success) {
|
||||
taskTags.value = result.dataList || [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 迁移检查清单
|
||||
|
||||
### 开发人员检查清单
|
||||
|
||||
- [x] ✅ 更新 Tag 类型定义(添加 tagType 字段)
|
||||
- [x] ✅ 添加 TagType 枚举
|
||||
- [x] ✅ 更新 resourceTag API(添加 getTagsByType 方法)
|
||||
- [x] ✅ 废弃 resourceCategory API(添加迁移说明)
|
||||
- [x] ✅ 更新 ArticleManagementView 组件
|
||||
- [x] ✅ 更新 ArticleAddView 组件
|
||||
- [x] ✅ 更新 ResourceSideBar 组件
|
||||
- [x] ✅ 添加课程标签说明文档
|
||||
- [x] ✅ 添加学习任务标签说明文档
|
||||
|
||||
### 未来开发任务
|
||||
|
||||
当开发课程管理或学习任务管理功能时:
|
||||
|
||||
- [ ] 在课程管理页面使用 `resourceTagApi.getTagsByType(2)` 获取课程分类标签
|
||||
- [ ] 在学习任务管理页面使用 `resourceTagApi.getTagsByType(3)` 获取任务分类标签
|
||||
- [ ] 为课程添加分类标签选择功能
|
||||
- [ ] 为学习任务添加分类标签选择功能
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 向后兼容性
|
||||
|
||||
- `ResourceCategory` 类型和 `resourceCategoryApi` 暂时保留,但标记为 `@deprecated`
|
||||
- 建议尽快迁移到新的标签 API
|
||||
- 旧代码仍可运行,但会在 IDE 中显示废弃警告
|
||||
|
||||
### 2. 字段映射变化
|
||||
|
||||
| ResourceCategory | Tag | 说明 |
|
||||
|-----------------|-----|------|
|
||||
| `tagID` | `tagID` | 唯一标识字段名变更 |
|
||||
| `name` | `name` | 名称字段保持不变 |
|
||||
| `description` | `description` | 描述字段保持不变 |
|
||||
| - | `tagType` | 新增:标签类型(1/2/3) |
|
||||
| - | `color` | 新增:标签颜色 |
|
||||
|
||||
### 3. 导入路径
|
||||
|
||||
所有标签相关的类型和 API 现在统一从以下位置导入:
|
||||
|
||||
```typescript
|
||||
// 类型导入
|
||||
import { Tag, TagType } from '@/types/resource';
|
||||
|
||||
// API 导入
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
本次前端标签系统改造:
|
||||
|
||||
1. ✅ **统一标签管理** - 文章、课程、学习任务使用统一的标签系统
|
||||
2. ✅ **类型化分类** - 通过 tagType 区分不同业务领域的标签
|
||||
3. ✅ **简化架构** - 废弃 ResourceCategory,统一使用 Tag
|
||||
4. ✅ **灵活扩展** - 为未来添加新标签类型预留空间
|
||||
5. ✅ **向后兼容** - 旧 API 仍可使用(标记为废弃)
|
||||
|
||||
### 迁移前后对比
|
||||
|
||||
| 项目 | 迁移前 | 迁移后 |
|
||||
|------|--------|--------|
|
||||
| 文章分类 | `resourceCategoryApi.getCategoryList()` | `resourceTagApi.getTagsByType(1)` |
|
||||
| 课程分类 | 无 | `resourceTagApi.getTagsByType(2)` |
|
||||
| 学习任务分类 | 无 | `resourceTagApi.getTagsByType(3)` |
|
||||
| 类型定义 | `ResourceCategory` | `Tag` (with tagType) |
|
||||
| 字段名 | `tagID` | `tagID` |
|
||||
|
||||
改造完成后,系统将具备更清晰的业务边界、更简洁的代码结构和更灵活的扩展能力。
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- 后端迁移文档:`schoolNewsServ/.bin/mysql/sql/TAG_TYPE_MIGRATION.md`
|
||||
- 后端移除清单:`schoolNewsServ/.bin/mysql/sql/RESOURCE_CATEGORY_REMOVAL.md`
|
||||
- 前端项目结构:`schoolNewsWeb/PROJECT_STRUCTURE.md`
|
||||
|
||||
@@ -199,17 +199,17 @@ export const resourceApi = {
|
||||
/**
|
||||
* 搜索资源
|
||||
* @param keyword 搜索关键词
|
||||
* @param categoryID 分类ID(可选)
|
||||
* @param tagID 分类ID(可选)
|
||||
* @param status 状态(可选)
|
||||
* @returns Promise<ResultDomain<Resource>>
|
||||
*/
|
||||
async searchResources(
|
||||
keyword: string,
|
||||
categoryID?: string,
|
||||
tagID?: string,
|
||||
status?: number
|
||||
): Promise<ResultDomain<Resource>> {
|
||||
const params: any = { keyword };
|
||||
if (categoryID) params.categoryID = categoryID;
|
||||
if (tagID) params.tagID = tagID;
|
||||
if (status !== undefined) params.status = status;
|
||||
|
||||
const response = await api.get<Resource>('/news/resources/search', params);
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-15
|
||||
*
|
||||
* ⚠️ 注意:此API已废弃!
|
||||
*
|
||||
* 从2025-10-27起,资源分类功能已迁移到标签系统(Tag)中。
|
||||
*
|
||||
* 迁移说明:
|
||||
* - 原 tb_resource_category 表已废弃
|
||||
* - 改为使用 tb_tag 表的 tag_type=1 表示文章分类标签
|
||||
* - 请使用 resourceTagApi.getTagsByType(1) 替代本 API 的方法
|
||||
*
|
||||
* API 迁移对照:
|
||||
* - getCategoryList() → resourceTagApi.getTagsByType(1)
|
||||
* - getCategoryById(id) → resourceTagApi.getTagById(id)
|
||||
* - createCategory(category) → resourceTagApi.createTag({...category, tagType: 1})
|
||||
* - updateCategory(category) → resourceTagApi.updateTag(category)
|
||||
* - deleteCategory(id) → resourceTagApi.deleteTag(id)
|
||||
*
|
||||
* @deprecated 请使用 resourceTagApi 代替
|
||||
*/
|
||||
|
||||
import { api } from '@/apis';
|
||||
@@ -11,6 +29,7 @@ import type { ResultDomain, ResourceCategory } from '@/types';
|
||||
|
||||
/**
|
||||
* 资源分类API服务
|
||||
* @deprecated 已废弃,请使用 resourceTagApi.getTagsByType(1) 获取文章分类标签
|
||||
*/
|
||||
export const resourceCategoryApi = {
|
||||
/**
|
||||
@@ -24,11 +43,11 @@ export const resourceCategoryApi = {
|
||||
|
||||
/**
|
||||
* 根据ID获取分类详情
|
||||
* @param categoryID 分类ID
|
||||
* @param tagID 分类ID
|
||||
* @returns Promise<ResultDomain<ResourceCategory>>
|
||||
*/
|
||||
async getCategoryById(categoryID: string): Promise<ResultDomain<ResourceCategory>> {
|
||||
const response = await api.get<ResourceCategory>(`/news/categorys/category/${categoryID}`);
|
||||
async getCategoryById(tagID: string): Promise<ResultDomain<ResourceCategory>> {
|
||||
const response = await api.get<ResourceCategory>(`/news/categorys/category/${tagID}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -54,22 +73,22 @@ export const resourceCategoryApi = {
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
* @param categoryID 分类ID
|
||||
* @param tagID 分类ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async deleteCategory(categoryID: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`/news/categorys/category/${categoryID}`);
|
||||
async deleteCategory(tagID: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`/news/categorys/category/${tagID}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新分类状态
|
||||
* @param categoryID 分类ID
|
||||
* @param tagID 分类ID
|
||||
* @param status 状态值
|
||||
* @returns Promise<ResultDomain<ResourceCategory>>
|
||||
*/
|
||||
async updateCategoryStatus(categoryID: string, status: number): Promise<ResultDomain<ResourceCategory>> {
|
||||
const response = await api.put<ResourceCategory>(`/news/categorys/category/${categoryID}/status`, null, {
|
||||
async updateCategoryStatus(tagID: string, status: number): Promise<ResultDomain<ResourceCategory>> {
|
||||
const response = await api.put<ResourceCategory>(`/news/categorys/category/${tagID}/status`, null, {
|
||||
params: { status }
|
||||
});
|
||||
return response.data;
|
||||
|
||||
@@ -16,7 +16,7 @@ export const resourceTagApi = {
|
||||
// ==================== 标签基础操作 ====================
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
* 获取标签列表(获取所有标签)
|
||||
* @returns Promise<ResultDomain<Tag>>
|
||||
*/
|
||||
async getTagList(): Promise<ResultDomain<Tag>> {
|
||||
@@ -24,6 +24,16 @@ export const resourceTagApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据标签类型获取标签列表
|
||||
* @param tagType 标签类型(1-文章分类标签 2-课程分类标签 3-学习任务分类标签)
|
||||
* @returns Promise<ResultDomain<Tag>>
|
||||
*/
|
||||
async getTagsByType(tagType: number): Promise<ResultDomain<Tag>> {
|
||||
const response = await api.get<Tag>(`/news/tags/type/${tagType}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据ID获取标签详情
|
||||
* @param tagID 标签ID
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
v-if="expanded && !collapsed"
|
||||
>
|
||||
<MenuItem
|
||||
v-for="child in menu.children"
|
||||
v-for="child in filteredChildren"
|
||||
:key="child.menuID"
|
||||
:menu="child"
|
||||
:collapsed="false"
|
||||
@@ -85,15 +85,23 @@ const emit = defineEmits<{
|
||||
'menu-click': [menu: SysMenu];
|
||||
}>();
|
||||
|
||||
// 状态
|
||||
const expanded = ref(false);
|
||||
// 状态 - 顶层菜单默认展开
|
||||
const expanded = ref(props.level === 0);
|
||||
|
||||
// Composition API
|
||||
const route = useRoute();
|
||||
|
||||
// 计算属性
|
||||
const hasChildren = computed(() => {
|
||||
return props.menu.children && props.menu.children.length > 0;
|
||||
// 只显示SIDEBAR类型的子菜单,过滤掉PAGE类型
|
||||
return props.menu.children &&
|
||||
props.menu.children.filter((child: SysMenu) => child.type === MenuType.SIDEBAR).length > 0;
|
||||
});
|
||||
|
||||
// 过滤后的子菜单(只显示SIDEBAR类型)
|
||||
const filteredChildren = computed(() => {
|
||||
if (!props.menu.children) return [];
|
||||
return props.menu.children.filter((child: SysMenu) => child.type === MenuType.SIDEBAR);
|
||||
});
|
||||
|
||||
const isActive = computed(() => {
|
||||
@@ -109,7 +117,8 @@ function toggleExpanded() {
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (props.menu.type === MenuType.NAVIGATION && props.menu.url) {
|
||||
// 支持NAVIGATION和SIDEBAR类型的菜单点击
|
||||
if (props.menu.url && (props.menu.type === MenuType.NAVIGATION || props.menu.type === MenuType.SIDEBAR)) {
|
||||
emit('menu-click', props.menu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
<!-- <div class="breadcrumb-wrapper" v-if="breadcrumbItems.length > 0">
|
||||
<Breadcrumb :items="breadcrumbItems" />
|
||||
</div> -->
|
||||
|
||||
<!-- 侧边栏和内容 -->
|
||||
<!-- 显示导航栏的侧边栏和内容 -->
|
||||
<div class="content-wrapper" v-if="hasSidebarMenus">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
|
||||
243
schoolNewsWeb/src/layouts/SidebarLayout.vue
Normal file
243
schoolNewsWeb/src/layouts/SidebarLayout.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="sidebar-layout">
|
||||
<!-- 顶部导航栏 -->
|
||||
<TopNavigation />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="layout-content">
|
||||
<!-- 侧边栏和内容 -->
|
||||
<div class="content-wrapper" v-if="hasSidebarMenus">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="sidebar-toggle-btn" @click="toggleSidebar">
|
||||
<i class="toggle-icon">{{ sidebarCollapsed ? '▶' : '◀' }}</i>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<MenuSidebar
|
||||
:menus="sidebarMenus"
|
||||
:collapsed="sidebarCollapsed"
|
||||
@menu-click="handleMenuClick"
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 没有侧边栏时直接显示内容 -->
|
||||
<div class="content-wrapper-full" v-else>
|
||||
<main class="main-content-full">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { TopNavigation, MenuSidebar } from '@/components';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const sidebarCollapsed = ref(false);
|
||||
|
||||
// 获取所有菜单
|
||||
const allMenus = computed(() => store.getters['auth/menuTree']);
|
||||
|
||||
// 获取所有顶层SIDEBAR菜单作为侧边栏
|
||||
const sidebarMenus = computed(() => {
|
||||
// 严格过滤:type=SIDEBAR 且 layout='SidebarLayout' 的顶层菜单
|
||||
return allMenus.value.filter((menu: SysMenu) =>
|
||||
menu.type === MenuType.SIDEBAR &&
|
||||
!menu.parentID &&
|
||||
(menu as any).layout === 'SidebarLayout'
|
||||
);
|
||||
});
|
||||
|
||||
// 是否有侧边栏菜单
|
||||
const hasSidebarMenus = computed(() => sidebarMenus.value.length > 0);
|
||||
|
||||
// 切换侧边栏
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed.value));
|
||||
}
|
||||
|
||||
// 处理菜单点击
|
||||
function handleMenuClick(menu: SysMenu) {
|
||||
if (menu.url && menu.url !== route.path) {
|
||||
router.push(menu.url);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复侧边栏状态
|
||||
const savedState = localStorage.getItem('sidebarCollapsed');
|
||||
if (savedState !== null) {
|
||||
sidebarCollapsed.value = savedState === 'true';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 76px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: #001529;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
transition: width 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
max-height: calc(100vh - 108px); // 顶部导航76px + 上下边距32px
|
||||
overflow: hidden;
|
||||
|
||||
&.collapsed {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -12px;
|
||||
width: 24px;
|
||||
height: 48px;
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 0 12px 12px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
|
||||
// 美化滚动条
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
height: calc(100vh - 108px); // 固定高度:视口高度 - 顶部导航76px - 上下边距32px
|
||||
max-height: calc(100vh - 108px);
|
||||
|
||||
// 美化滚动条
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper-full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main-content-full {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
height: calc(100vh - 108px);
|
||||
overflow-y: auto;
|
||||
|
||||
// 美化滚动条
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,8 +20,8 @@ export interface Resource extends BaseDTO {
|
||||
summary?: string;
|
||||
/** 封面图片 */
|
||||
coverImage?: string;
|
||||
/** 分类ID */
|
||||
categoryID?: string;
|
||||
/** 标签ID(文章分类标签,tagType=1) */
|
||||
tagID?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 来源 */
|
||||
@@ -81,10 +81,11 @@ export interface Banner extends BaseDTO {
|
||||
|
||||
/**
|
||||
* 资源分类实体
|
||||
* @deprecated 已废弃,请使用 Tag 接口(tagType=1表示文章分类标签)
|
||||
*/
|
||||
export interface ResourceCategory extends BaseDTO {
|
||||
/** 分类唯一标识 */
|
||||
categoryID?: string;
|
||||
tagID?: string;
|
||||
/** 分类名称 */
|
||||
name?: string;
|
||||
/** 父分类ID */
|
||||
@@ -111,6 +112,18 @@ export interface ResourceTag extends BaseDTO {
|
||||
tagID?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签类型枚举
|
||||
*/
|
||||
export enum TagType {
|
||||
/** 文章分类标签 */
|
||||
ARTICLE_CATEGORY = 1,
|
||||
/** 课程分类标签 */
|
||||
COURSE_CATEGORY = 2,
|
||||
/** 学习任务分类标签 */
|
||||
LEARNING_TASK_CATEGORY = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签实体
|
||||
*/
|
||||
@@ -123,15 +136,21 @@ export interface Tag extends BaseDTO {
|
||||
description?: string;
|
||||
/** 标签颜色 */
|
||||
color?: string;
|
||||
/** 标签类型(1-文章分类标签 2-课程分类标签 3-学习任务分类标签) */
|
||||
tagType?: number;
|
||||
/** 排序号 */
|
||||
orderNum?: number;
|
||||
/** 状态(0禁用 1启用) */
|
||||
status?: number;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
/** 更新者 */
|
||||
updater?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ResourceVO extends BaseDTO{
|
||||
resource: Resource;
|
||||
category: ResourceCategory;
|
||||
tags: Tag[];
|
||||
}
|
||||
export interface tagVO extends BaseDTO{
|
||||
@@ -198,8 +217,8 @@ export interface DataCollectionLog extends BaseDTO {
|
||||
export interface ResourceSearchParams {
|
||||
/** 关键词 */
|
||||
keyword?: string;
|
||||
/** 分类ID */
|
||||
categoryID?: string;
|
||||
/** 标签ID(文章分类标签,tagType=1) */
|
||||
tagID?: string;
|
||||
/** 状态 */
|
||||
status?: number;
|
||||
/** 是否推荐 */
|
||||
|
||||
@@ -71,11 +71,18 @@ export interface CourseChapter extends BaseDTO {
|
||||
|
||||
/**
|
||||
* 课程标签关联实体
|
||||
*
|
||||
* 说明:课程标签使用统一的标签系统(tb_tag表)
|
||||
* - 使用 tagType=2 表示课程分类标签
|
||||
* - 通过 resourceTagApi.getTagsByType(2) 或 resourceTagApi.getTagsByType(TagType.COURSE_CATEGORY) 获取课程分类标签
|
||||
*
|
||||
* @see Tag - 标签实体定义
|
||||
* @see TagType - 标签类型枚举(COURSE_CATEGORY = 2)
|
||||
*/
|
||||
export interface CourseTag extends BaseDTO {
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 标签ID */
|
||||
/** 标签ID(关联 tb_tag 表,tagType=2 的标签) */
|
||||
tagID?: string;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
@@ -124,6 +131,13 @@ export interface CourseVO extends BaseDTO {
|
||||
|
||||
/**
|
||||
* 学习任务实体
|
||||
*
|
||||
* 说明:学习任务分类使用统一的标签系统(tb_tag表)
|
||||
* - 使用 tagType=3 表示学习任务分类标签
|
||||
* - 通过 resourceTagApi.getTagsByType(3) 或 resourceTagApi.getTagsByType(TagType.LEARNING_TASK_CATEGORY) 获取学习任务分类标签
|
||||
*
|
||||
* @see Tag - 标签实体定义
|
||||
* @see TagType - 标签类型枚举(LEARNING_TASK_CATEGORY = 3)
|
||||
*/
|
||||
export interface LearningTask extends BaseDTO {
|
||||
/** 任务唯一标识 */
|
||||
|
||||
@@ -17,6 +17,8 @@ const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
||||
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
|
||||
// 导航布局(新版,顶部导航+动态侧边栏)
|
||||
'NavigationLayout': () => import('@/layouts/NavigationLayout.vue'),
|
||||
// 侧边栏布局(管理后台专用,顶层SIDEBAR菜单)
|
||||
'SidebarLayout': () => import('@/layouts/SidebarLayout.vue'),
|
||||
// 空白布局
|
||||
'BlankLayout': () => import('@/layouts/BlankLayout.vue'),
|
||||
// 页面布局
|
||||
@@ -126,10 +128,11 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
||||
route.component = getComponent(layout);
|
||||
} else if (hasChildren && isTopLevel) {
|
||||
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
|
||||
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
|
||||
if (menu.type === MenuType.NAVIGATION) {
|
||||
route.component = getComponent('NavigationLayout');
|
||||
} else if (menu.type === MenuType.SIDEBAR) {
|
||||
route.component = getComponent('BlankLayout');
|
||||
} else if (menu.type === MenuType.SIDEBAR && !menu.parentID) {
|
||||
// 顶层SIDEBAR菜单(管理后台)默认使用SidebarLayout
|
||||
route.component = getComponent('SidebarLayout');
|
||||
} else {
|
||||
route.component = getComponent('BasicLayout');
|
||||
}
|
||||
|
||||
@@ -7,55 +7,283 @@
|
||||
placeholder="搜索标签..."
|
||||
style="width: 300px"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="tags" style="width: 100%">
|
||||
<el-table :data="filteredTags" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" label="标签名称" min-width="150" />
|
||||
<el-table-column prop="category" label="标签分类" width="120" />
|
||||
<el-table-column prop="color" label="颜色" width="100">
|
||||
<el-table-column prop="tagType" label="标签类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="color-preview" :style="{ background: row.color }"></div>
|
||||
<el-tag :type="getTagTypeColor(row.tagType)">
|
||||
{{ getTagTypeName(row.tagType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="color" label="颜色" width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="color-display">
|
||||
<div class="color-preview" :style="{ background: row.color }"></div>
|
||||
<span>{{ row.color }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="80" />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="usageCount" label="使用次数" width="100" />
|
||||
<el-table-column prop="createDate" label="创建时间" width="150" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editTag(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteTag(row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="editTag(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteTag(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 创建/编辑标签对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑标签' : '新增标签'"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form :model="currentTag" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="currentTag.name" placeholder="请输入标签名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签类型" prop="tagType">
|
||||
<el-select v-model="currentTag.tagType" placeholder="请选择标签类型" style="width: 100%">
|
||||
<el-option label="文章分类标签" :value="1" />
|
||||
<el-option label="课程分类标签" :value="2" />
|
||||
<el-option label="学习任务分类标签" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签颜色" prop="color">
|
||||
<div class="color-picker-wrapper">
|
||||
<el-color-picker v-model="currentTag.color" />
|
||||
<el-input v-model="currentTag.color" placeholder="#000000" style="width: 150px; margin-left: 10px" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签描述">
|
||||
<el-input
|
||||
v-model="currentTag.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入标签描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序号" prop="orderNum">
|
||||
<el-input-number v-model="currentTag.orderNum" :min="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="currentTag.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElButton, ElInput, ElTable, ElTableColumn, ElMessage } from 'element-plus';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import type { Tag } from '@/types/resource';
|
||||
|
||||
const searchKeyword = ref('');
|
||||
const tags = ref<any[]>([]);
|
||||
const tags = ref<Tag[]>([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const currentTag = ref<Partial<Tag>>({
|
||||
name: '',
|
||||
tagType: 1,
|
||||
color: '#409EFF',
|
||||
description: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入标签名称', trigger: 'blur' }
|
||||
],
|
||||
tagType: [
|
||||
{ required: true, message: '请选择标签类型', trigger: 'change' }
|
||||
],
|
||||
color: [
|
||||
{ required: true, message: '请选择标签颜色', trigger: 'change' }
|
||||
]
|
||||
};
|
||||
|
||||
// 过滤后的标签列表
|
||||
const filteredTags = computed(() => {
|
||||
if (!searchKeyword.value) return tags.value;
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
return tags.value.filter(tag =>
|
||||
tag.name?.toLowerCase().includes(keyword) ||
|
||||
tag.description?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadTags();
|
||||
});
|
||||
|
||||
function loadTags() {
|
||||
// TODO: 加载标签数据
|
||||
// 加载标签列表
|
||||
async function loadTags() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await resourceTagApi.getTagList();
|
||||
if (result.success) {
|
||||
tags.value = result.dataList || [];
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载标签列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载标签列表失败:', error);
|
||||
ElMessage.error('加载标签列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
function handleSearch() {
|
||||
// 搜索由computed自动处理
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
function showCreateDialog() {
|
||||
// TODO: 显示创建标签对话框
|
||||
isEdit.value = false;
|
||||
currentTag.value = {
|
||||
name: '',
|
||||
tagType: 1,
|
||||
color: '#409EFF',
|
||||
description: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function editTag(row: any) {
|
||||
// TODO: 编辑标签
|
||||
// 编辑标签
|
||||
function editTag(row: Tag) {
|
||||
isEdit.value = true;
|
||||
currentTag.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function deleteTag(row: any) {
|
||||
// TODO: 删除标签
|
||||
ElMessage.success('删除成功');
|
||||
// 删除标签
|
||||
async function deleteTag(row: Tag) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除标签"${row.name}"吗?删除后不可恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const result = await resourceTagApi.deleteTag(row.tagID!);
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功');
|
||||
loadTags();
|
||||
} else {
|
||||
ElMessage.error(result.message || '删除失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除标签失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
|
||||
let result;
|
||||
if (isEdit.value) {
|
||||
result = await resourceTagApi.updateTag(currentTag.value as Tag);
|
||||
} else {
|
||||
result = await resourceTagApi.createTag(currentTag.value as Tag);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
dialogVisible.value = false;
|
||||
loadTags();
|
||||
} else {
|
||||
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框关闭处理
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
// 获取标签类型名称
|
||||
function getTagTypeName(type?: number): string {
|
||||
const map: Record<number, string> = {
|
||||
1: '文章分类',
|
||||
2: '课程分类',
|
||||
3: '任务分类'
|
||||
};
|
||||
return map[type || 1] || '未知';
|
||||
}
|
||||
|
||||
// 获取标签类型颜色
|
||||
function getTagTypeColor(type?: number): string {
|
||||
const map: Record<number, string> = {
|
||||
1: 'primary',
|
||||
2: 'success',
|
||||
3: 'warning'
|
||||
};
|
||||
return map[type || 1] || '';
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string): string {
|
||||
if (!time) return '-';
|
||||
return new Date(time).toLocaleString('zh-CN');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -71,11 +299,21 @@ function deleteTag(row: any) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.color-picker-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { resourceApi, resourceCategoryApi } from '@/apis/resource'
|
||||
import type { PageParam, ResourceSearchParams, Resource, ResourceCategory } from '@/types';
|
||||
import { resourceApi, resourceTagApi } from '@/apis/resource'
|
||||
import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types';
|
||||
import { ArticleShowView } from '@/views/article';
|
||||
import { ArticleStatus } from '@/types/enums';
|
||||
|
||||
@@ -86,7 +86,7 @@ const total = ref<number>(0);
|
||||
const articles = ref<Resource[]>([]);
|
||||
const showViewDialog = ref(false);
|
||||
const currentArticle = ref<any>(null);
|
||||
const categoryList = ref<ResourceCategory[]>([]);
|
||||
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
|
||||
onMounted(() => {
|
||||
loadArticles();
|
||||
@@ -95,7 +95,8 @@ onMounted(() => {
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await resourceCategoryApi.getCategoryList();
|
||||
// 使用新的标签API获取文章分类标签(tagType=1)
|
||||
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
||||
if (res.success && res.dataList) {
|
||||
categoryList.value = res.dataList;
|
||||
}
|
||||
|
||||
@@ -15,25 +15,13 @@
|
||||
<!-- 分类和标签 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文章分类" prop="resource.categoryID">
|
||||
<el-select v-model="articleForm.resource.categoryID" placeholder="请选择分类" style="width: 100%" :loading="categoryLoading">
|
||||
<el-form-item label="文章分类" prop="resource.tagID">
|
||||
<el-select v-model="articleForm.resource.tagID" placeholder="请选择分类" style="width: 100%" :loading="categoryLoading" value-key="tagID">
|
||||
<el-option
|
||||
v-for="category in categoryList"
|
||||
:key="category.categoryID || category.id"
|
||||
:key="category.tagID || category.id"
|
||||
:label="category.name"
|
||||
:value="category.categoryID || category.id || ''"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="标签" prop="tags">
|
||||
<el-select v-model="articleForm.tags" multiple placeholder="请选择标签" style="width: 100%" :loading="tagLoading">
|
||||
<el-option
|
||||
v-for="tag in tagList"
|
||||
:key="tag.id || tag.tagID"
|
||||
:label="tag.name"
|
||||
:value="tag.id || tag.tagID || ''"
|
||||
:value="category.tagID||''"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -127,8 +115,8 @@ import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { RichTextComponent } from '@/components/text';
|
||||
import { FileUpload } from '@/components/file';
|
||||
import { ArticleShowView } from './index';
|
||||
import { resourceCategoryApi, resourceTagApi, resourceApi } from '@/apis/resource';
|
||||
import { ResourceVO, ResourceCategory, Tag } from '@/types/resource';
|
||||
import { resourceTagApi, resourceApi } from '@/apis/resource';
|
||||
import { ResourceVO, Tag, TagType } from '@/types/resource';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -143,7 +131,7 @@ const previewVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
|
||||
// 数据状态
|
||||
const categoryList = ref<ResourceCategory[]>([]);
|
||||
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
const tagList = ref<Tag[]>([]);
|
||||
const categoryLoading = ref(false);
|
||||
const tagLoading = ref(false);
|
||||
@@ -152,7 +140,6 @@ const tagLoading = ref(false);
|
||||
const articleForm = ref<ResourceVO>({
|
||||
resource: {
|
||||
},
|
||||
category: {},
|
||||
tags: [],
|
||||
});
|
||||
|
||||
@@ -162,7 +149,7 @@ const rules = {
|
||||
{ required: true, message: '请输入文章标题', trigger: 'blur' },
|
||||
{ min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
'resource.categoryID': [
|
||||
'resource.tagID': [
|
||||
{ required: true, message: '请选择文章分类', trigger: 'change' }
|
||||
],
|
||||
'resource.content': [
|
||||
@@ -171,11 +158,12 @@ const rules = {
|
||||
};
|
||||
|
||||
|
||||
// 加载分类列表
|
||||
// 加载分类列表(使用标签API,tagType=1表示文章分类标签)
|
||||
async function loadCategoryList() {
|
||||
try {
|
||||
categoryLoading.value = true;
|
||||
const result = await resourceCategoryApi.getCategoryList();
|
||||
// 使用新的标签API获取文章分类标签(tagType=1)
|
||||
const result = await resourceTagApi.getTagsByType(TagType.ARTICLE_CATEGORY);
|
||||
if (result.success) {
|
||||
// 数组数据从 dataList 获取
|
||||
categoryList.value = result.dataList || [];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<h1 class="platform-title">红色思政学习平台</h1>
|
||||
</div>
|
||||
<h2 class="login-title">账号登陆</h2>
|
||||
<h2 class="login-title">账号登录</h2>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
@@ -38,7 +38,7 @@
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入学号"
|
||||
placeholder="请输入学号、手机号"
|
||||
class="form-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -76,7 +76,7 @@
|
||||
登录
|
||||
</el-button>
|
||||
<p class="agreement-text">
|
||||
登录即为同意<span class="agreement-link" style="color: red">《红色思政智能体平台》</span>
|
||||
<el-checkbox v-model="loginForm.rememberMe">登录即为同意<span class="agreement-link" style="color: red">《红色思政智能体平台》</span></el-checkbox>
|
||||
</p>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="content-container">
|
||||
<ResourceSideBar
|
||||
:active-category-id="currentCategoryId"
|
||||
:activeTagID="currentCategoryId"
|
||||
@category-change="handleCategoryChange"
|
||||
/>
|
||||
<ResourceList
|
||||
v-if="!showArticle"
|
||||
ref="resourceListRef"
|
||||
:category-id="currentCategoryId"
|
||||
:tagID="currentCategoryId"
|
||||
:search-keyword="searchKeyword"
|
||||
@resource-click="handleResourceClick"
|
||||
@list-updated="handleListUpdated"
|
||||
@@ -25,7 +25,7 @@
|
||||
<ResourceArticle
|
||||
v-if="showArticle"
|
||||
:resource-id="currentResourceId"
|
||||
:category-id="currentCategoryId"
|
||||
:tagID="currentCategoryId"
|
||||
:resource-list="resourceList"
|
||||
@resource-change="handleResourceChange"
|
||||
@navigate="handleArticleNavigate"
|
||||
@@ -40,7 +40,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { ResourceSideBar, ResourceList, ResourceArticle } from './components';
|
||||
import { Search, CenterHead } from '@/components/base';
|
||||
import type { Resource, ResourceCategory } from '@/types/resource';
|
||||
import type { Resource, Tag } from '@/types/resource';
|
||||
|
||||
const showArticle = ref(false);
|
||||
const currentCategoryId = ref('party_history');
|
||||
@@ -50,8 +50,8 @@ const searchKeyword = ref('');
|
||||
const resourceListRef = ref();
|
||||
const resourceList = ref<Resource[]>([]);
|
||||
|
||||
function handleCategoryChange(category: ResourceCategory) {
|
||||
currentCategoryId.value = category.categoryID || '';
|
||||
function handleCategoryChange(category: Tag) {
|
||||
currentCategoryId.value = category.tagID || category.id || '';
|
||||
currentCategoryName.value = category.name || '';
|
||||
searchKeyword.value = '';
|
||||
showArticle.value = false;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { CollectionType, type UserCollection } from '@/types';
|
||||
|
||||
interface Props {
|
||||
resourceId?: string;
|
||||
categoryId?: string;
|
||||
tagID?: string;
|
||||
resourceList?: Resource[];
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -48,7 +48,7 @@ import type { PageParam } from '@/types';
|
||||
import defaultArticleImg from '@/assets/imgs/article-default.png';
|
||||
|
||||
interface Props {
|
||||
categoryId?: string;
|
||||
tagID?: string;
|
||||
searchKeyword?: string;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ onMounted(() => {
|
||||
loadResources();
|
||||
});
|
||||
|
||||
watch(() => [props.categoryId, props.searchKeyword], () => {
|
||||
watch(() => [props.tagID, props.searchKeyword], () => {
|
||||
currentPage.value = 1;
|
||||
loadResources();
|
||||
}, { deep: true });
|
||||
@@ -82,14 +82,14 @@ async function loadResources() {
|
||||
|
||||
try {
|
||||
const filter: ResourceSearchParams = {
|
||||
categoryID: props.categoryId,
|
||||
tagID: props.tagID,
|
||||
keyword: props.searchKeyword,
|
||||
// status: 1 // 只加载已发布的
|
||||
};
|
||||
|
||||
const pageParam: PageParam = {
|
||||
page: currentPage.value,
|
||||
size: pageSize
|
||||
pageNumber: currentPage.value,
|
||||
pageSize: pageSize
|
||||
};
|
||||
|
||||
const res = await resourceApi.getResourcePage(pageParam, filter);
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="sidebar-content">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.categoryID"
|
||||
:key="category.tagID || category.id"
|
||||
class="sidebar-item"
|
||||
:class="{ active: category.categoryID === activeCategoryId }"
|
||||
:class="{ active: (category.tagID || category.id) === activeTagID }"
|
||||
@click="handleCategoryClick(category)"
|
||||
>
|
||||
<span class="category-name">{{ category.name }}</span>
|
||||
<div v-if="category.categoryID === activeCategoryId" class="active-overlay"></div>
|
||||
<div v-if="(category.tagID || category.id) === activeTagID" class="active-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,22 +17,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { resourceCategoryApi } from '@/apis/resource';
|
||||
import type { ResourceCategory } from '@/types/resource';
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import type { Tag, TagType } from '@/types/resource';
|
||||
|
||||
interface Props {
|
||||
activeCategoryId?: string;
|
||||
activeTagID?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
activeCategoryId: 'party_history'
|
||||
activeTagID: 'party_history'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'category-change': [category: ResourceCategory];
|
||||
'category-change': [category: Tag]; // 改为Tag类型
|
||||
}>();
|
||||
|
||||
const categories = ref<ResourceCategory[]>([]);
|
||||
const categories = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategories();
|
||||
@@ -40,7 +40,8 @@ onMounted(async () => {
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await resourceCategoryApi.getCategoryList();
|
||||
// 使用新的标签API获取文章分类标签(tagType=1)
|
||||
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
||||
if (res.success && res.dataList) {
|
||||
categories.value = res.dataList;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ async function loadCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCategoryClick(category: ResourceCategory) {
|
||||
function handleCategoryClick(category: Tag) {
|
||||
emit('category-change', category);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user