serv\web-侧边栏 标签统一

This commit is contained in:
2025-10-27 16:21:00 +08:00
parent e50de4a277
commit 5fa4e1cd42
47 changed files with 1933 additions and 1307 deletions

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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 = {
};
// 加载分类列表
// 加载分类列表使用标签APItagType=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 || [];

View File

@@ -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>

View File

@@ -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;

View File

@@ -33,7 +33,7 @@ import { CollectionType, type UserCollection } from '@/types';
interface Props {
resourceId?: string;
categoryId?: string;
tagID?: string;
resourceList?: Resource[];
}
const props = defineProps<Props>();

View File

@@ -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);

View File

@@ -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>