视图修改、接口修改

This commit is contained in:
2025-10-28 19:04:35 +08:00
parent 98c73632bd
commit c5c134fbb3
96 changed files with 7122 additions and 4194 deletions

View File

@@ -1,12 +1,16 @@
<template>
<div class="achievement-management">
<div class="header">
<h2>成就管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增成就
</el-button>
</div>
<AdminLayout
title="成就管理"
subtitle="管理用户成就、徽章等激励内容"
>
<div class="achievement-management">
<div class="header">
<h2>成就管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增成就
</el-button>
</div>
<!-- 筛选条件 -->
<div class="filter-bar">
@@ -305,7 +309,8 @@
</el-table-column>
</el-table>
</el-dialog>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -316,6 +321,11 @@ import { achievementApi } from '@/apis/achievement';
import type { Achievement, UserAchievement } from '@/types';
import { AchievementEnumHelper } from '@/types/enums/achievement-enums';
import { getAchievementIconUrl } from '@/utils/iconUtils';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'AchievementManagementView'
});
// 响应式数据
const loading = ref(false);
@@ -544,7 +554,9 @@ onMounted(() => {
<style scoped lang="scss">
.achievement-management {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
}
.header {

View File

@@ -1,75 +1,80 @@
<template>
<div class="ai-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">模型配置</el-divider>
<el-form-item label="AI模型">
<el-select v-model="configForm.model" placeholder="选择AI模型">
<el-option label="GPT-3.5" value="gpt-3.5" />
<el-option label="GPT-4" value="gpt-4" />
<el-option label="Claude" value="claude" />
</el-select>
</el-form-item>
<AdminLayout title="AI配置" subtitle="AI配置">
<div class="ai-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">模型配置</el-divider>
<el-form-item label="AI模型">
<el-select v-model="configForm.model" placeholder="选择AI模型">
<el-option label="GPT-3.5" value="gpt-3.5" />
<el-option label="GPT-4" value="gpt-4" />
<el-option label="Claude" value="claude" />
</el-select>
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="configForm.apiKey" type="password" show-password />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="configForm.apiKey" type="password" show-password />
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="configForm.apiUrl" />
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="configForm.apiUrl" />
</el-form-item>
<el-divider content-position="left">对话配置</el-divider>
<el-divider content-position="left">对话配置</el-divider>
<el-form-item label="温度值">
<el-slider v-model="configForm.temperature" :min="0" :max="2" :step="0.1" show-input />
<span class="help-text">控制回答的随机性值越大回答越随机</span>
</el-form-item>
<el-form-item label="温度值">
<el-slider v-model="configForm.temperature" :min="0" :max="2" :step="0.1" show-input />
<span class="help-text">控制回答的随机性值越大回答越随机</span>
</el-form-item>
<el-form-item label="最大token数">
<el-input-number v-model="configForm.maxTokens" :min="100" :max="4000" />
</el-form-item>
<el-form-item label="最大token数">
<el-input-number v-model="configForm.maxTokens" :min="100" :max="4000" />
</el-form-item>
<el-form-item label="历史对话轮数">
<el-input-number v-model="configForm.historyTurns" :min="1" :max="20" />
</el-form-item>
<el-form-item label="历史对话轮数">
<el-input-number v-model="configForm.historyTurns" :min="1" :max="20" />
</el-form-item>
<el-divider content-position="left">功能配置</el-divider>
<el-divider content-position="left">功能配置</el-divider>
<el-form-item label="启用流式输出">
<el-switch v-model="configForm.enableStreaming" />
</el-form-item>
<el-form-item label="启用流式输出">
<el-switch v-model="configForm.enableStreaming" />
</el-form-item>
<el-form-item label="启用文件解读">
<el-switch v-model="configForm.enableFileInterpretation" />
</el-form-item>
<el-form-item label="启用文件解读">
<el-switch v-model="configForm.enableFileInterpretation" />
</el-form-item>
<el-form-item label="启用知识库检索">
<el-switch v-model="configForm.enableKnowledgeRetrieval" />
</el-form-item>
<el-form-item label="启用知识库检索">
<el-switch v-model="configForm.enableKnowledgeRetrieval" />
</el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="configForm.systemPrompt"
type="textarea"
:rows="6"
placeholder="设置AI助手的角色和行为..."
/>
</el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="configForm.systemPrompt"
type="textarea"
:rows="6"
placeholder="设置AI助手的角色和行为..."
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleTest">测试连接</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleTest">测试连接</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElSlider, ElInputNumber, ElSwitch, ElButton, ElDivider, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'AIConfigView'
});
const configForm = ref({
model: 'gpt-3.5',
apiKey: '',

View File

@@ -1,16 +1,18 @@
<template>
<div class="ai-management">
<h1 class="page-title">智能体管理</h1>
<AdminLayout title="智能体管理" subtitle="智能体管理">
<div class="ai-management">
<h1 class="page-title">智能体管理</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="基础配置" name="config">
<AIConfig />
</el-tab-pane>
<el-tab-pane label="知识库管理" name="knowledge">
<KnowledgeManagement />
</el-tab-pane>
</el-tabs>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="基础配置" name="config">
<AIConfig />
</el-tab-pane>
<el-tab-pane label="知识库管理" name="knowledge">
<KnowledgeManagement />
</el-tab-pane>
</el-tabs>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -18,7 +20,10 @@ import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import AIConfig from './components/AIConfig.vue';
import KnowledgeManagement from './components/KnowledgeManagement.vue';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'AIManagementView'
});
const activeTab = ref('config');
</script>

View File

@@ -1,58 +1,63 @@
<template>
<div class="knowledge-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增知识</el-button>
<el-button @click="handleImport">批量导入</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索知识..."
style="width: 300px"
clearable
<AdminLayout title="知识库管理" subtitle="知识库管理">
<div class="knowledge-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增知识</el-button>
<el-button @click="handleImport">批量导入</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索知识..."
style="width: 300px"
clearable
/>
</div>
<el-table :data="knowledgeList" style="width: 100%">
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="tags" label="标签" width="200">
<template #default="{ row }">
<el-tag v-for="tag in row.tags" :key="tag" size="small" style="margin-right: 4px;">
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.status"
@change="toggleStatus(row)"
/>
</template>
</el-table-column>
<el-table-column prop="updateDate" label="更新时间" width="150" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editKnowledge(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteKnowledge(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<el-table :data="knowledgeList" style="width: 100%">
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="tags" label="标签" width="200">
<template #default="{ row }">
<el-tag v-for="tag in row.tags" :key="tag" size="small" style="margin-right: 4px;">
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.status"
@change="toggleStatus(row)"
/>
</template>
</el-table-column>
<el-table-column prop="updateDate" label="更新时间" width="150" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editKnowledge(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteKnowledge(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElSwitch, ElPagination, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'KnowledgeManagementView'
});
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);

View File

@@ -1,33 +1,43 @@
<template>
<div class="column-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增栏目</el-button>
</div>
<AdminLayout
title="内容管理"
subtitle="管理网站横幅、栏目、标签等内容信息"
>
<div class="column-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增栏目</el-button>
</div>
<el-table :data="columns" style="width: 100%" row-key="id" :tree-props="{children: 'children'}">
<el-table-column prop="name" label="栏目名称" min-width="200" />
<el-table-column prop="code" label="栏目编码" width="150" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'info'">
{{ row.status ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editColumn(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteColumn(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-table :data="columns" style="width: 100%" row-key="id" :tree-props="{children: 'children'}">
<el-table-column prop="name" label="栏目名称" min-width="200" />
<el-table-column prop="code" label="栏目编码" width="150" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'info'">
{{ row.status ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editColumn(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteColumn(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElTable, ElTableColumn, ElTag, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'ColumnManagementView'
});
const columns = ref<any[]>([]);
@@ -55,11 +65,12 @@ function deleteColumn(row: any) {
<style lang="scss" scoped>
.column-management {
background: #FFFFFF;
padding: 20px;
border-radius: 14px;
}
.action-bar {
margin-bottom: 20px;
}
</style>

View File

@@ -1,41 +1,30 @@
<template>
<div class="language-management">
<h1 class="page-title">语言管理</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="Banner管理" name="banner">
<BannerManagement />
</el-tab-pane>
<el-tab-pane label="资源栏目管理" name="column">
<ColumnManagement />
</el-tab-pane>
<el-tab-pane label="标签管理" name="tag">
<TagManagement />
</el-tab-pane>
</el-tabs>
</div>
<AdminLayout
title="内容管理"
subtitle="管理网站横幅、栏目、标签等内容信息"
>
<div class="content-management">
<el-empty description="请使用顶部标签页切换到Banner管理、标签管理或栏目管理" />
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import BannerManagement from './components/BannerManagement.vue';
import ColumnManagement from './components/ColumnManagement.vue';
import TagManagement from './components/TagManagement.vue';
import { AdminLayout } from '@/views/admin';
const activeTab = ref('banner');
defineOptions({
name: 'ContentManagementView'
});
</script>
<style lang="scss" scoped>
.language-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
.content-management {
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,53 +1,49 @@
<template>
<div class="tag-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增标签</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索标签..."
style="width: 300px"
clearable
@change="handleSearch"
/>
</div>
<AdminLayout
title="标签管理"
subtitle="各类标签信息"
>
<div class="tag-management">
<!-- 顶部操作栏 -->
<div class="header">
<h2 class="title">数据标签管理</h2>
<button class="add-button" @click="showCreateDialog">
<img src="@/assets/imgs/plus.svg" alt="添加" />
<span>添加标签</span>
</button>
</div>
<el-table :data="filteredTags" style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="标签名称" min-width="150" />
<el-table-column prop="tagType" label="标签类型" width="120">
<template #default="{ row }">
<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 v-loading="loading" class="tag-grid">
<div
v-for="tag in tags"
:key="tag.tagID"
class="tag-card"
>
<div class="tag-content">
<div class="tag-info">
<div class="tag-dot" :style="{ background: tag.color }"></div>
<span class="tag-name">{{ tag.name }}</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 label="操作" width="180" fixed="right">
<template #default="{ row }">
<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>
<!-- <div class="tag-badge">{{ tag.usageCount || 0 }}</div> -->
</div>
<div class="tag-actions">
<button class="edit-button" @click="editTag(tag)">
<img src="@/assets/imgs/edit.svg" alt="编辑" />
<span>编辑</span>
</button>
<button class="delete-button" @click="deleteTag(tag)">
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
</button>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && tags.length === 0" class="empty-state">
暂无标签数据
</div>
</div>
<!-- 创建/编辑标签对话框 -->
<el-dialog
@@ -84,17 +80,6 @@
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>
@@ -102,16 +87,21 @@
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
</template>
</el-dialog>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { resourceTagApi } from '@/apis/resource';
import type { Tag } from '@/types/resource';
import {AdminLayout} from '@/views/admin';
defineOptions({
name: 'TagManagementView'
});
const searchKeyword = ref('');
const tags = ref<Tag[]>([]);
const loading = ref(false);
const dialogVisible = ref(false);
@@ -123,9 +113,7 @@ const currentTag = ref<Partial<Tag>>({
name: '',
tagType: 1,
color: '#409EFF',
description: '',
orderNum: 0,
status: 1
description: ''
});
const rules: FormRules = {
@@ -140,16 +128,6 @@ const rules: FormRules = {
]
};
// 过滤后的标签列表
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();
});
@@ -161,6 +139,8 @@ async function loadTags() {
const result = await resourceTagApi.getTagList();
if (result.success) {
tags.value = result.dataList || [];
// TODO: 加载每个标签的使用计数
// 这里可以调用 API 获取每个标签被使用的资源数量
} else {
ElMessage.error(result.message || '加载标签列表失败');
}
@@ -172,11 +152,6 @@ async function loadTags() {
}
}
// 搜索处理
function handleSearch() {
// 搜索由computed自动处理
}
// 显示创建对话框
function showCreateDialog() {
isEdit.value = false;
@@ -184,9 +159,7 @@ function showCreateDialog() {
name: '',
tagType: 1,
color: '#409EFF',
description: '',
orderNum: 0,
status: 1
description: ''
};
dialogVisible.value = true;
}
@@ -259,61 +232,208 @@ async function handleSubmit() {
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>
<style lang="scss" scoped>
.tag-management {
padding: 20px;
display: flex;
flex-direction: column;
gap: 48px;
box-sizing: border-box;
background: white;
padding: 24px;
border-radius: 14px;
}
.action-bar {
.header {
display: flex;
gap: 16px;
margin-bottom: 20px;
justify-content: space-between;
align-items: center;
}
.color-display {
.title {
margin: 0;
font-family: 'Inter', 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 1.5em;
letter-spacing: -0.02em;
color: #101828;
}
.add-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
background: #E7000B;
border: none;
border-radius: 8px;
color: #FFFFFF;
font-family: 'Inter', 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 14px;
line-height: 1.43em;
letter-spacing: -0.01em;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #c50009;
}
&:active {
background: #a80008;
}
svg {
flex-shrink: 0;
}
}
.tag-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
min-height: 200px;
}
.tag-card {
display: flex;
flex-direction: column;
gap: 36px;
padding: 16px;
background: #F9FAFB;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
box-sizing: border-box;
}
.tag-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.tag-info {
display: flex;
align-items: center;
gap: 8px;
}
.color-preview {
width: 40px;
height: 24px;
border-radius: 4px;
border: 1px solid #e0e0e0;
.tag-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.tag-name {
font-family: 'Inter', 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 1.5em;
letter-spacing: -0.02em;
color: #101828;
}
.tag-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
background: #ECEEF2;
border: 1px solid transparent;
border-radius: 8px;
font-family: 'Inter', 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 12px;
line-height: 1.33em;
color: #030213;
}
.tag-actions {
display: flex;
gap: 8px;
}
.edit-button {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 32px;
padding: 0 16px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
font-family: 'Inter', 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 14px;
line-height: 1.43em;
letter-spacing: -0.01em;
color: #0A0A0A;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
}
svg {
flex-shrink: 0;
}
}
.delete-button {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 32px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #fff5f5;
border-color: rgba(231, 0, 11, 0.2);
}
svg {
flex-shrink: 0;
}
}
.empty-state {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
padding: 60px 0;
font-size: 14px;
color: #909399;
}
.color-picker-wrapper {
display: flex;
align-items: center;
}
// 响应式调整
@media (max-width: 1400px) {
.tag-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 900px) {
.tag-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,218 +1,220 @@
<template>
<div class="log-management">
<div class="header">
<h2>执行日志</h2>
<el-button type="danger" @click="handleCleanLogs">
<el-icon><Delete /></el-icon>
清理日志
</el-button>
</div>
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">任务名称</span>
<el-input
v-model="searchForm.taskName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">任务分组</span>
<el-input
v-model="searchForm.taskGroup"
placeholder="请输入任务分组"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">执行状态</span>
<el-select
v-model="searchForm.executeStatus"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
<AdminLayout title="执行日志" subtitle="执行日志管理">
<div class="log-management">
<div class="header">
<h2>执行日志</h2>
<el-button type="danger" @click="handleCleanLogs">
<el-icon><Delete /></el-icon>
清理日志
</el-button>
</div>
</div>
<!-- 日志列表 -->
<el-table
:data="logList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="taskName" label="任务名称" min-width="150" />
<el-table-column prop="taskGroup" label="任务分组" width="120" />
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
<el-table-column prop="methodName" label="方法名称" width="120" />
<el-table-column label="执行状态" width="100">
<template #default="{ row }">
<el-tag :type="row.executeStatus === 1 ? 'success' : 'danger'" size="small">
{{ row.executeStatus === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="executeDuration" label="执行时长" width="100">
<template #default="{ row }">
{{ row.executeDuration }}ms
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="endTime" label="结束时间" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleViewDetail(row)"
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">任务名称</span>
<el-input
v-model="searchForm.taskName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">任务分组</span>
<el-input
v-model="searchForm.taskGroup"
placeholder="请输入任务分组"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">执行状态</span>
<el-select
v-model="searchForm.executeStatus"
placeholder="请选择状态"
clearable
style="width: 150px"
>
查看详情
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
</div>
<!-- 日志列表 -->
<el-table
:data="logList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="taskName" label="任务名称" min-width="150" />
<el-table-column prop="taskGroup" label="任务分组" width="120" />
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
<el-table-column prop="methodName" label="方法名称" width="120" />
<el-table-column label="执行状态" width="100">
<template #default="{ row }">
<el-tag :type="row.executeStatus === 1 ? 'success' : 'danger'" size="small">
{{ row.executeStatus === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="executeDuration" label="执行时长" width="100">
<template #default="{ row }">
{{ row.executeDuration }}ms
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="endTime" label="结束时间" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleViewDetail(row)"
>
查看详情
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="执行日志详情"
width="700px"
>
<div class="detail-content" v-if="currentLog">
<div class="detail-item">
<span class="detail-label">任务名称</span>
<span class="detail-value">{{ currentLog.taskName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">任务分组</span>
<span class="detail-value">{{ currentLog.taskGroup }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Bean名称</span>
<span class="detail-value">{{ currentLog.beanName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">方法名称</span>
<span class="detail-value">{{ currentLog.methodName }}</span>
</div>
<div class="detail-item" v-if="currentLog.methodParams">
<span class="detail-label">方法参数</span>
<span class="detail-value">{{ currentLog.methodParams }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
</el-tag>
</div>
<div class="detail-item">
<span class="detail-label">执行时长</span>
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
</div>
<div class="detail-item">
<span class="detail-label">开始时间</span>
<span class="detail-value">{{ currentLog.startTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">结束时间</span>
<span class="detail-value">{{ currentLog.endTime }}</span>
</div>
<div class="detail-item" v-if="currentLog.executeMessage">
<span class="detail-label">执行结果</span>
<div class="detail-message">{{ currentLog.executeMessage }}</div>
</div>
<div class="detail-item" v-if="currentLog.exceptionInfo">
<span class="detail-label">异常信息</span>
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 清理日志对话框 -->
<el-dialog
v-model="cleanDialogVisible"
title="清理日志"
width="400px"
>
<div class="clean-dialog-content">
<el-alert
title="清理操作不可恢复,请谨慎操作!"
type="warning"
:closable="false"
show-icon
/>
<div class="clean-item">
<span class="clean-label">保留天数</span>
<el-input-number
v-model="cleanDays"
:min="1"
:max="365"
controls-position="right"
/>
<span class="clean-tip"></span>
</div>
<div class="clean-desc">
将删除 {{ cleanDays }} 天前的所有执行日志
</div>
</div>
<template #footer>
<el-button @click="cleanDialogVisible = false">取消</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
@click="handleConfirmClean"
:loading="submitting"
>
删除
确定清理
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-dialog>
</div>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="执行日志详情"
width="700px"
>
<div class="detail-content" v-if="currentLog">
<div class="detail-item">
<span class="detail-label">任务名称</span>
<span class="detail-value">{{ currentLog.taskName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">任务分组</span>
<span class="detail-value">{{ currentLog.taskGroup }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Bean名称</span>
<span class="detail-value">{{ currentLog.beanName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">方法名称</span>
<span class="detail-value">{{ currentLog.methodName }}</span>
</div>
<div class="detail-item" v-if="currentLog.methodParams">
<span class="detail-label">方法参数</span>
<span class="detail-value">{{ currentLog.methodParams }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
</el-tag>
</div>
<div class="detail-item">
<span class="detail-label">执行时长</span>
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
</div>
<div class="detail-item">
<span class="detail-label">开始时间</span>
<span class="detail-value">{{ currentLog.startTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">结束时间</span>
<span class="detail-value">{{ currentLog.endTime }}</span>
</div>
<div class="detail-item" v-if="currentLog.executeMessage">
<span class="detail-label">执行结果</span>
<div class="detail-message">{{ currentLog.executeMessage }}</div>
</div>
<div class="detail-item" v-if="currentLog.exceptionInfo">
<span class="detail-label">异常信息</span>
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 清理日志对话框 -->
<el-dialog
v-model="cleanDialogVisible"
title="清理日志"
width="400px"
>
<div class="clean-dialog-content">
<el-alert
title="清理操作不可恢复,请谨慎操作!"
type="warning"
:closable="false"
show-icon
/>
<div class="clean-item">
<span class="clean-label">保留天数</span>
<el-input-number
v-model="cleanDays"
:min="1"
:max="365"
controls-position="right"
/>
<span class="clean-tip"></span>
</div>
<div class="clean-desc">
将删除 {{ cleanDays }} 天前的所有执行日志
</div>
</div>
<template #footer>
<el-button @click="cleanDialogVisible = false">取消</el-button>
<el-button
type="danger"
@click="handleConfirmClean"
:loading="submitting"
>
确定清理
</el-button>
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -221,7 +223,10 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Search, Refresh } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabLog, PageParam } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'LogManagementView'
});
// 数据状态
const loading = ref(false);
const submitting = ref(false);

View File

@@ -1,266 +1,268 @@
<template>
<div class="news-crawler">
<div class="header">
<h2>新闻爬虫配置</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增爬虫
</el-button>
</div>
<AdminLayout title="新闻爬虫" subtitle="新闻爬虫配置">
<div class="news-crawler">
<div class="header">
<h2>新闻爬虫配置</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增爬虫
</el-button>
</div>
<!-- 说明卡片 -->
<el-alert
title="新闻爬虫说明"
type="info"
:closable="false"
style="margin-bottom: 20px"
>
<p>新闻爬虫配置允许系统自动从指定的新闻源抓取最新新闻内容</p>
<p>配置完成后系统会按照设定的Cron表达式定时执行爬取任务</p>
</el-alert>
<!-- 说明卡片 -->
<el-alert
title="新闻爬虫说明"
type="info"
:closable="false"
style="margin-bottom: 20px"
>
<p>新闻爬虫配置允许系统自动从指定的新闻源抓取最新新闻内容</p>
<p>配置完成后系统会按照设定的Cron表达式定时执行爬取任务</p>
</el-alert>
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">爬虫名称</span>
<el-input
v-model="searchForm.taskName"
placeholder="请输入爬虫名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">爬虫名称</span>
<el-input
v-model="searchForm.taskName"
placeholder="请输入爬虫名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">状态</span>
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="运行中" :value="1" />
<el-option label="已暂停" :value="0" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
</div>
<!-- 爬虫配置列表 -->
<div class="crawler-list">
<el-row :gutter="20">
<el-col :span="8" v-for="crawler in crawlerList" :key="crawler.taskId">
<el-card class="crawler-card" shadow="hover">
<template #header>
<div class="card-header">
<div class="card-title">
<el-icon class="title-icon"><DocumentCopy /></el-icon>
<span>{{ crawler.taskName }}</span>
</div>
<el-tag
:type="crawler.status === 1 ? 'success' : 'info'"
size="small"
>
{{ crawler.status === 1 ? '运行中' : '已暂停' }}
</el-tag>
</div>
</template>
<div class="card-content">
<div class="info-item">
<span class="info-label">Bean名称</span>
<span class="info-value">{{ crawler.beanName }}</span>
</div>
<div class="info-item">
<span class="info-label">方法名称</span>
<span class="info-value">{{ crawler.methodName }}</span>
</div>
<div class="info-item">
<span class="info-label">执行周期</span>
<span class="info-value">{{ crawler.cronExpression }}</span>
</div>
<div class="info-item" v-if="crawler.description">
<span class="info-label">描述</span>
<span class="info-value">{{ crawler.description }}</span>
</div>
</div>
<template #footer>
<div class="card-actions">
<el-button
v-if="crawler.status === 0"
type="success"
size="small"
@click="handleStart(crawler)"
>
<el-icon><VideoPlay /></el-icon>
启动
</el-button>
<el-button
v-else
type="warning"
size="small"
@click="handlePause(crawler)"
>
<el-icon><VideoPause /></el-icon>
暂停
</el-button>
<el-button
type="primary"
size="small"
@click="handleExecute(crawler)"
>
<el-icon><Promotion /></el-icon>
执行一次
</el-button>
<el-button
type="primary"
size="small"
@click="handleEdit(crawler)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(crawler)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</template>
</el-card>
</el-col>
</el-row>
</div>
<!-- 空状态 -->
<el-empty
v-if="!loading && crawlerList.length === 0"
description="暂无爬虫配置"
style="margin-top: 40px"
/>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[9, 18, 36]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<div class="search-item">
<span class="search-label">状态</span>
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="运行中" :value="1" />
<el-option label="已暂停" :value="0" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
</div>
<!-- 爬虫配置列表 -->
<div class="crawler-list">
<el-row :gutter="20">
<el-col :span="8" v-for="crawler in crawlerList" :key="crawler.taskId">
<el-card class="crawler-card" shadow="hover">
<template #header>
<div class="card-header">
<div class="card-title">
<el-icon class="title-icon"><DocumentCopy /></el-icon>
<span>{{ crawler.taskName }}</span>
</div>
<el-tag
:type="crawler.status === 1 ? 'success' : 'info'"
size="small"
>
{{ crawler.status === 1 ? '运行中' : '已暂停' }}
</el-tag>
</div>
</template>
<div class="card-content">
<div class="info-item">
<span class="info-label">Bean名称</span>
<span class="info-value">{{ crawler.beanName }}</span>
</div>
<div class="info-item">
<span class="info-label">方法名称</span>
<span class="info-value">{{ crawler.methodName }}</span>
</div>
<div class="info-item">
<span class="info-label">执行周期</span>
<span class="info-value">{{ crawler.cronExpression }}</span>
</div>
<div class="info-item" v-if="crawler.description">
<span class="info-label">描述</span>
<span class="info-value">{{ crawler.description }}</span>
</div>
</div>
<template #footer>
<div class="card-actions">
<el-button
v-if="crawler.status === 0"
type="success"
size="small"
@click="handleStart(crawler)"
>
<el-icon><VideoPlay /></el-icon>
启动
</el-button>
<el-button
v-else
type="warning"
size="small"
@click="handlePause(crawler)"
>
<el-icon><VideoPause /></el-icon>
暂停
</el-button>
<el-button
type="primary"
size="small"
@click="handleExecute(crawler)"
>
<el-icon><Promotion /></el-icon>
执行一次
</el-button>
<el-button
type="primary"
size="small"
@click="handleEdit(crawler)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(crawler)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</template>
</el-card>
</el-col>
</el-row>
</div>
<!-- 空状态 -->
<el-empty
v-if="!loading && crawlerList.length === 0"
description="暂无爬虫配置"
style="margin-top: 40px"
/>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[9, 18, 36]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑爬虫' : '新增爬虫'"
width="700px"
@close="resetForm"
>
<div class="form-content">
<div class="form-item">
<span class="form-label required">爬虫名称</span>
<el-input
v-model="formData.taskName"
placeholder="请输入爬虫名称"
clearable
/>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑爬虫' : '新增爬虫'"
width="700px"
@close="resetForm"
>
<div class="form-content">
<div class="form-item">
<span class="form-label required">爬虫名称</span>
<el-input
v-model="formData.taskName"
placeholder="请输入爬虫名称"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">Bean名称</span>
<el-input
v-model="formData.beanName"
placeholder="请输入Spring Bean名称newsCrawlerTask"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">方法名称</span>
<el-input
v-model="formData.methodName"
placeholder="请输入要执行的方法名crawlNews"
clearable
/>
</div>
<div class="form-item">
<span class="form-label">方法参数</span>
<el-input
v-model="formData.methodParams"
type="textarea"
:rows="3"
placeholder="请输入方法参数JSON格式可选"
clearable
/>
<span class="form-tip">
示例{"source":"xinhua","category":"education"}
</span>
</div>
<div class="form-item">
<span class="form-label required">Cron表达式</span>
<el-input
v-model="formData.cronExpression"
placeholder="请输入Cron表达式"
clearable
>
<template #append>
<el-button @click="validateCron">验证</el-button>
</template>
</el-input>
<span class="form-tip">
常用示例<br/>
- 0 0 */6 * * ? 每6小时执行一次<br/>
- 0 0 8,12,18 * * ? 每天8点1218点执行<br/>
- 0 0/30 * * * ? 每30分钟执行一次
</span>
</div>
<div class="form-item">
<span class="form-label">爬虫描述</span>
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入爬虫描述"
/>
</div>
<div class="form-item">
<span class="form-label">是否允许并发</span>
<el-radio-group v-model="formData.concurrent">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">禁止</el-radio>
</el-radio-group>
<span class="form-tip">
建议禁止并发避免重复抓取
</span>
</div>
</div>
<div class="form-item">
<span class="form-label required">Bean名称</span>
<el-input
v-model="formData.beanName"
placeholder="请输入Spring Bean名称newsCrawlerTask"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">方法名称</span>
<el-input
v-model="formData.methodName"
placeholder="请输入要执行的方法名crawlNews"
clearable
/>
</div>
<div class="form-item">
<span class="form-label">方法参数</span>
<el-input
v-model="formData.methodParams"
type="textarea"
:rows="3"
placeholder="请输入方法参数JSON格式可选"
clearable
/>
<span class="form-tip">
示例{"source":"xinhua","category":"education"}
</span>
</div>
<div class="form-item">
<span class="form-label required">Cron表达式</span>
<el-input
v-model="formData.cronExpression"
placeholder="请输入Cron表达式"
clearable
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
<template #append>
<el-button @click="validateCron">验证</el-button>
</template>
</el-input>
<span class="form-tip">
常用示例<br/>
- 0 0 */6 * * ? 每6小时执行一次<br/>
- 0 0 8,12,18 * * ? 每天8点1218点执行<br/>
- 0 0/30 * * * ? 每30分钟执行一次
</span>
</div>
<div class="form-item">
<span class="form-label">爬虫描述</span>
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入爬虫描述"
/>
</div>
<div class="form-item">
<span class="form-label">是否允许并发</span>
<el-radio-group v-model="formData.concurrent">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">禁止</el-radio>
</el-radio-group>
<span class="form-tip">
建议禁止并发避免重复抓取
</span>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
</el-button>
</template>
</el-dialog>
</div>
确定
</el-button>
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -269,7 +271,10 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Refresh, DocumentCopy, VideoPlay, VideoPause, Promotion, Edit, Delete } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabTask, PageParam } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'NewsCrawlerView'
});
// 数据状态
const loading = ref(false);
const submitting = ref(false);

View File

@@ -1,245 +1,247 @@
<template>
<div class="task-management">
<div class="header">
<h2>定时任务管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增任务
</el-button>
</div>
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">任务名称</span>
<el-input
v-model="searchForm.taskName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">任务分组</span>
<el-input
v-model="searchForm.taskGroup"
placeholder="请输入任务分组"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="search-label">状态</span>
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="运行中" :value="1" />
<el-option label="已暂停" :value="0" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
<AdminLayout title="定时任务" subtitle="定时任务管理">
<div class="task-management">
<div class="header">
<h2>定时任务管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增任务
</el-button>
</div>
</div>
<!-- 任务列表 -->
<el-table
:data="taskList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="taskName" label="任务名称" min-width="150" />
<el-table-column prop="taskGroup" label="任务分组" width="120" />
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
<el-table-column prop="methodName" label="方法名称" width="120" />
<el-table-column prop="cronExpression" label="Cron表达式" min-width="150" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '运行中' : '已暂停' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="并发" width="80">
<template #default="{ row }">
<el-tag :type="row.concurrent === 1 ? 'success' : 'warning'" size="small">
{{ row.concurrent === 1 ? '允许' : '禁止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 0"
type="success"
size="small"
@click="handleStart(row)"
>
启动
</el-button>
<el-button
v-else
type="warning"
size="small"
@click="handlePause(row)"
>
暂停
</el-button>
<el-button
type="primary"
size="small"
@click="handleExecute(row)"
>
执行一次
</el-button>
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑任务' : '新增任务'"
width="700px"
@close="resetForm"
>
<div class="form-content">
<div class="form-item">
<span class="form-label required">任务名称</span>
<!-- 搜索筛选区域 -->
<div class="search-bar">
<div class="search-item">
<span class="search-label">任务名称</span>
<el-input
v-model="formData.taskName"
placeholder="请输入任务名称"
v-model="searchForm.taskName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="form-item">
<span class="form-label required">任务分组</span>
<div class="search-item">
<span class="search-label">任务分组</span>
<el-input
v-model="formData.taskGroup"
placeholder="请输入任务分组SYSTEM、BUSINESS"
v-model="searchForm.taskGroup"
placeholder="请输入任务分组"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</div>
<div class="form-item">
<span class="form-label required">Bean名称</span>
<el-input
v-model="formData.beanName"
placeholder="请输入Spring Bean名称"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">方法名称</span>
<el-input
v-model="formData.methodName"
placeholder="请输入要执行的方法名"
clearable
/>
</div>
<div class="form-item">
<span class="form-label">方法参数</span>
<el-input
v-model="formData.methodParams"
placeholder="请输入方法参数JSON格式可选"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">Cron表达式</span>
<el-input
v-model="formData.cronExpression"
placeholder="请输入Cron表达式0 0 2 * * ?"
<div class="search-item">
<span class="search-label">状态</span>
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<template #append>
<el-button @click="validateCron">验证</el-button>
</template>
</el-input>
<span class="form-tip">
格式 年可选
示例0 0 2 * * ? 表示每天凌晨2点执行
</span>
</div>
<div class="form-item">
<span class="form-label">是否允许并发</span>
<el-radio-group v-model="formData.concurrent">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">禁止</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">错过执行策略</span>
<el-select v-model="formData.misfirePolicy" placeholder="请选择策略">
<el-option label="立即执行" :value="1" />
<el-option label="执行一次" :value="2" />
<el-option label="放弃执行" :value="3" />
<el-option label="运行中" :value="1" />
<el-option label="已暂停" :value="0" />
</el-select>
</div>
<div class="form-item">
<span class="form-label">任务描述</span>
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入任务描述"
/>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
</el-button>
</template>
</el-dialog>
</div>
<!-- 任务列表 -->
<el-table
:data="taskList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="taskName" label="任务名称" min-width="150" />
<el-table-column prop="taskGroup" label="任务分组" width="120" />
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
<el-table-column prop="methodName" label="方法名称" width="120" />
<el-table-column prop="cronExpression" label="Cron表达式" min-width="150" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '运行中' : '已暂停' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="并发" width="80">
<template #default="{ row }">
<el-tag :type="row.concurrent === 1 ? 'success' : 'warning'" size="small">
{{ row.concurrent === 1 ? '允许' : '禁止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 0"
type="success"
size="small"
@click="handleStart(row)"
>
启动
</el-button>
<el-button
v-else
type="warning"
size="small"
@click="handlePause(row)"
>
暂停
</el-button>
<el-button
type="primary"
size="small"
@click="handleExecute(row)"
>
执行一次
</el-button>
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑任务' : '新增任务'"
width="700px"
@close="resetForm"
>
<div class="form-content">
<div class="form-item">
<span class="form-label required">任务名称</span>
<el-input
v-model="formData.taskName"
placeholder="请输入任务名称"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">任务分组</span>
<el-input
v-model="formData.taskGroup"
placeholder="请输入任务分组SYSTEM、BUSINESS"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">Bean名称</span>
<el-input
v-model="formData.beanName"
placeholder="请输入Spring Bean名称"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">方法名称</span>
<el-input
v-model="formData.methodName"
placeholder="请输入要执行的方法名"
clearable
/>
</div>
<div class="form-item">
<span class="form-label">方法参数</span>
<el-input
v-model="formData.methodParams"
placeholder="请输入方法参数JSON格式可选"
clearable
/>
</div>
<div class="form-item">
<span class="form-label required">Cron表达式</span>
<el-input
v-model="formData.cronExpression"
placeholder="请输入Cron表达式0 0 2 * * ?"
clearable
>
<template #append>
<el-button @click="validateCron">验证</el-button>
</template>
</el-input>
<span class="form-tip">
格式 年可选
示例0 0 2 * * ? 表示每天凌晨2点执行
</span>
</div>
<div class="form-item">
<span class="form-label">是否允许并发</span>
<el-radio-group v-model="formData.concurrent">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">禁止</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">错过执行策略</span>
<el-select v-model="formData.misfirePolicy" placeholder="请选择策略">
<el-option label="立即执行" :value="1" />
<el-option label="执行一次" :value="2" />
<el-option label="放弃执行" :value="3" />
</el-select>
</div>
<div class="form-item">
<span class="form-label">任务描述</span>
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入任务描述"
/>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
</el-button>
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -248,7 +250,10 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Refresh } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabTask, PageParam } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'TaskManagementView'
});
// 数据状态
const loading = ref(false);
const submitting = ref(false);

View File

@@ -1,60 +1,65 @@
<template>
<div class="login-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名..."
style="width: 200px"
clearable
<AdminLayout title="登录日志" subtitle="登录日志管理">
<div class="login-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名..."
style="width: 200px"
clearable
/>
<el-select v-model="loginStatus" placeholder="登录状态" style="width: 150px" clearable>
<el-option label="成功" value="success" />
<el-option label="失败" value="failed" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
<el-button type="danger" @click="handleClear">清空日志</el-button>
</div>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="location" label="登录地点" width="150" />
<el-table-column prop="browser" label="浏览器" width="120" />
<el-table-column prop="os" label="操作系统" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="信息" min-width="150" />
<el-table-column prop="loginTime" label="登录时间" width="180" />
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-select v-model="loginStatus" placeholder="登录状态" style="width: 150px" clearable>
<el-option label="成功" value="success" />
<el-option label="失败" value="failed" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
<el-button type="danger" @click="handleClear">清空日志</el-button>
</div>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="location" label="登录地点" width="150" />
<el-table-column prop="browser" label="浏览器" width="120" />
<el-table-column prop="os" label="操作系统" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="信息" min-width="150" />
<el-table-column prop="loginTime" label="登录时间" width="180" />
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'LoginLogsView'
});
const searchKeyword = ref('');
const loginStatus = ref('');
const dateRange = ref<[Date, Date] | null>(null);

View File

@@ -1,72 +1,78 @@
<template>
<div class="operation-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名或操作..."
style="width: 250px"
clearable
<AdminLayout title="操作日志" subtitle="操作日志管理">
<div class="operation-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名或操作..."
style="width: 250px"
clearable
/>
<el-select v-model="operationType" placeholder="操作类型" style="width: 150px" clearable>
<el-option label="新增" value="create" />
<el-option label="修改" value="update" />
<el-option label="删除" value="delete" />
<el-option label="查询" value="read" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="操作人" width="120" />
<el-table-column prop="module" label="操作模块" width="120" />
<el-table-column prop="operation" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operation)">
{{ getOperationText(row.operation) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="操作描述" min-width="200" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="duration" label="耗时(ms)" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationTime" label="操作时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-select v-model="operationType" placeholder="操作类型" style="width: 150px" clearable>
<el-option label="新增" value="create" />
<el-option label="修改" value="update" />
<el-option label="删除" value="delete" />
<el-option label="查询" value="read" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
</AdminLayout>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="操作人" width="120" />
<el-table-column prop="module" label="操作模块" width="120" />
<el-table-column prop="operation" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operation)">
{{ getOperationText(row.operation) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="操作描述" min-width="200" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="duration" label="耗时(ms)" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationTime" label="操作时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'OperationLogsView'
});
const searchKeyword = ref('');
const operationType = ref('');
const dateRange = ref<[Date, Date] | null>(null);

View File

@@ -1,98 +1,103 @@
<template>
<div class="system-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">基本设置</el-divider>
<el-form-item label="系统名称">
<el-input v-model="configForm.systemName" />
</el-form-item>
<AdminLayout title="系统配置" subtitle="系统配置">
<div class="system-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">基本设置</el-divider>
<el-form-item label="系统名称">
<el-input v-model="configForm.systemName" />
</el-form-item>
<el-form-item label="系统Logo">
<el-upload
class="logo-uploader"
action="#"
:show-file-list="false"
:before-upload="beforeLogoUpload"
>
<img v-if="configForm.logo" :src="configForm.logo" class="logo-preview" />
<el-icon v-else class="logo-uploader-icon">+</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="系统Logo">
<el-upload
class="logo-uploader"
action="#"
:show-file-list="false"
:before-upload="beforeLogoUpload"
>
<img v-if="configForm.logo" :src="configForm.logo" class="logo-preview" />
<el-icon v-else class="logo-uploader-icon">+</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="版权信息">
<el-input v-model="configForm.copyright" />
</el-form-item>
<el-form-item label="版权信息">
<el-input v-model="configForm.copyright" />
</el-form-item>
<el-divider content-position="left">安全设置</el-divider>
<el-divider content-position="left">安全设置</el-divider>
<el-form-item label="启用验证码">
<el-switch v-model="configForm.enableCaptcha" />
</el-form-item>
<el-form-item label="启用验证码">
<el-switch v-model="configForm.enableCaptcha" />
</el-form-item>
<el-form-item label="密码最小长度">
<el-input-number v-model="configForm.minPasswordLength" :min="6" :max="20" />
</el-form-item>
<el-form-item label="密码最小长度">
<el-input-number v-model="configForm.minPasswordLength" :min="6" :max="20" />
</el-form-item>
<el-form-item label="会话超时(分钟)">
<el-input-number v-model="configForm.sessionTimeout" :min="5" :max="1440" />
</el-form-item>
<el-form-item label="会话超时(分钟)">
<el-input-number v-model="configForm.sessionTimeout" :min="5" :max="1440" />
</el-form-item>
<el-form-item label="登录失败锁定">
<el-switch v-model="configForm.enableLoginLock" />
</el-form-item>
<el-form-item label="登录失败锁定">
<el-switch v-model="configForm.enableLoginLock" />
</el-form-item>
<el-form-item label="锁定阈值(次)" v-if="configForm.enableLoginLock">
<el-input-number v-model="configForm.loginLockThreshold" :min="3" :max="10" />
</el-form-item>
<el-form-item label="锁定阈值(次)" v-if="configForm.enableLoginLock">
<el-input-number v-model="configForm.loginLockThreshold" :min="3" :max="10" />
</el-form-item>
<el-divider content-position="left">功能设置</el-divider>
<el-divider content-position="left">功能设置</el-divider>
<el-form-item label="启用用户注册">
<el-switch v-model="configForm.enableRegister" />
</el-form-item>
<el-form-item label="启用用户注册">
<el-switch v-model="configForm.enableRegister" />
</el-form-item>
<el-form-item label="启用评论功能">
<el-switch v-model="configForm.enableComment" />
</el-form-item>
<el-form-item label="启用评论功能">
<el-switch v-model="configForm.enableComment" />
</el-form-item>
<el-form-item label="启用文件上传">
<el-switch v-model="configForm.enableFileUpload" />
</el-form-item>
<el-form-item label="启用文件上传">
<el-switch v-model="configForm.enableFileUpload" />
</el-form-item>
<el-form-item label="文件上传大小限制(MB)">
<el-input-number v-model="configForm.maxFileSize" :min="1" :max="100" />
</el-form-item>
<el-form-item label="文件上传大小限制(MB)">
<el-input-number v-model="configForm.maxFileSize" :min="1" :max="100" />
</el-form-item>
<el-divider content-position="left">邮件设置</el-divider>
<el-divider content-position="left">邮件设置</el-divider>
<el-form-item label="启用邮件通知">
<el-switch v-model="configForm.enableEmail" />
</el-form-item>
<el-form-item label="启用邮件通知">
<el-switch v-model="configForm.enableEmail" />
</el-form-item>
<el-form-item label="SMTP服务器" v-if="configForm.enableEmail">
<el-input v-model="configForm.smtpHost" />
</el-form-item>
<el-form-item label="SMTP服务器" v-if="configForm.enableEmail">
<el-input v-model="configForm.smtpHost" />
</el-form-item>
<el-form-item label="SMTP端口" v-if="configForm.enableEmail">
<el-input-number v-model="configForm.smtpPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="SMTP端口" v-if="configForm.enableEmail">
<el-input-number v-model="configForm.smtpPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="发件人邮箱" v-if="configForm.enableEmail">
<el-input v-model="configForm.senderEmail" />
</el-form-item>
<el-form-item label="发件人邮箱" v-if="configForm.enableEmail">
<el-input v-model="configForm.senderEmail" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElInput, ElInputNumber, ElSwitch, ElButton, ElDivider, ElUpload, ElIcon, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'SystemConfigView'
});
const configForm = ref({
systemName: '红色思政学习平台',
logo: '',

View File

@@ -1,19 +1,21 @@
<template>
<div class="system-logs">
<h1 class="page-title">系统日志</h1>
<AdminLayout title="系统日志" subtitle="系统日志管理">
<div class="system-logs">
<h1 class="page-title">系统日志</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="登录日志" name="login">
<LoginLogs />
</el-tab-pane>
<el-tab-pane label="操作日志" name="operation">
<OperationLogs />
</el-tab-pane>
<el-tab-pane label="系统配置" name="config">
<SystemConfig />
</el-tab-pane>
</el-tabs>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="登录日志" name="login">
<LoginLogs />
</el-tab-pane>
<el-tab-pane label="操作日志" name="operation">
<OperationLogs />
</el-tab-pane>
<el-tab-pane label="系统配置" name="config">
<SystemConfig />
</el-tab-pane>
</el-tabs>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -22,7 +24,10 @@ import { ElTabs, ElTabPane } from 'element-plus';
import LoginLogs from './components/LoginLogs.vue';
import OperationLogs from './components/OperationLogs.vue';
import SystemConfig from './components/SystemConfig.vue';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'SystemLogsView'
});
const activeTab = ref('login');
</script>

View File

@@ -1,15 +1,19 @@
<template>
<div class="article-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增文章</el-button>
<el-button @click="handleDataCollection">数据采集</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索文章..."
style="width: 300px"
clearable
/>
</div>
<AdminLayout
title="资源管理"
subtitle="管理文章、资源、数据等内容"
>
<div class="article-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增文章</el-button>
<el-button @click="handleDataCollection">数据采集</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索文章..."
style="width: 300px"
clearable
/>
</div>
<el-table :data="articles" style="width: 100%">
<el-table-column prop="title" label="文章标题" min-width="200" />
@@ -61,10 +65,16 @@
@edit="handleEditFromView"
@close="showViewDialog = false"
/>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'ArticleManagementView'
});
import { ref, onMounted } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
import { useRouter } from 'vue-router';
@@ -245,7 +255,9 @@ function handleCurrentChange(val: number) {
<style lang="scss" scoped>
.article-management {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
}
.action-bar {

View File

@@ -1,21 +1,31 @@
<template>
<div class="data-records">
<el-tabs v-model="activeTab">
<el-tab-pane label="菜单管理" name="menu">
<!-- 菜单管理已在manage/system中实现这里可以引用或重新实现 -->
<div class="redirect-info">
<p>菜单管理功能已在系统管理模块中实现</p>
<el-button type="primary" @click="goToMenuManage">前往菜单管理</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
<AdminLayout
title="资源管理"
subtitle="管理文章、资源、数据等内容"
>
<div class="data-records">
<el-tabs v-model="activeTab">
<el-tab-pane label="菜单管理" name="menu">
<!-- 菜单管理已在manage/system中实现这里可以引用或重新实现 -->
<div class="redirect-info">
<p>菜单管理功能已在系统管理模块中实现</p>
<el-button type="primary" @click="goToMenuManage">前往菜单管理</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElTabs, ElTabPane, ElButton } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'DataRecordsView'
});
const router = useRouter();
const activeTab = ref('menu');
@@ -27,7 +37,9 @@ function goToMenuManage() {
<style lang="scss" scoped>
.data-records {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
}
.redirect-info {

View File

@@ -1,37 +1,30 @@
<template>
<div class="resource-management">
<h1 class="page-title">资源管理</h1>
<el-tabs v-model="activeTab" class="resource-tabs">
<el-tab-pane label="文章储备" name="articles">
<ArticleManagement />
</el-tab-pane>
<el-tab-pane label="数据记录" name="data">
<DataRecords />
</el-tab-pane>
</el-tabs>
</div>
<AdminLayout
title="资源管理"
subtitle="管理文章、资源、数据等内容"
>
<div class="resource-management">
<el-empty description="请使用顶部标签页切换到对应的资源管理功能" />
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import ArticleManagement from './components/ArticleManagement.vue';
import DataRecords from './components/DataRecords.vue';
import { AdminLayout } from '@/views/admin';
const activeTab = ref('articles');
defineOptions({
name: 'ResourceManagementView'
});
</script>
<style lang="scss" scoped>
.resource-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,23 +1,33 @@
<template>
<div class="course-management">
<CourseList
v-if="currentView === 'list'"
@add="handleAdd"
@edit="handleEdit"
/>
<CourseAdd
v-else-if="currentView === 'add' || currentView === 'edit'"
:courseID="currentCourseId"
@success="handleSuccess"
@cancel="handleCancel"
/>
</div>
<AdminLayout
title="学习管理"
subtitle="管理课程、学习任务、学习记录等信息"
>
<div class="course-management">
<CourseList
v-if="currentView === 'list'"
@add="handleAdd"
@edit="handleEdit"
/>
<CourseAdd
v-else-if="currentView === 'add' || currentView === 'edit'"
:courseID="currentCourseId"
@success="handleSuccess"
@cancel="handleCancel"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { CourseList, CourseAdd } from '@/views/public/course/components';
import type { Course } from '@/types/study';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'CourseManagementView'
});
type ViewType = 'list' | 'add' | 'edit';
@@ -47,7 +57,8 @@ function handleCancel() {
<style lang="scss" scoped>
.course-management {
height: 100%;
background: #f5f7fa;
background: #FFFFFF;
border-radius: 14px;
min-height: 500px;
}
</style>

View File

@@ -1,22 +1,29 @@
<template>
<div class="study-management">
<h1 class="page-title">学习管理</h1>
<AdminLayout title="学习管理" subtitle="学习记录管理">
<div class="study-management">
<h1 class="page-title">学习管理</h1>
<el-tabs v-model="activeTab">
<el-tabs v-model="activeTab">
<el-tab-pane label="学习记录" name="task-records">
<StudyRecords />
</el-tab-pane>
</el-tabs>
</div>
</AdminLayout>
<el-tab-pane label="学习记录" name="task-records">
<StudyRecords />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import StudyRecords from './components/StudyRecords.vue';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'StudyManagementView'
});
const activeTab = ref('task-publish');
</script>
<style lang="scss" scoped>

View File

@@ -1,70 +1,75 @@
<template>
<div class="study-records">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户..."
style="width: 200px"
clearable
/>
<el-select v-model="selectedTask" placeholder="选择任务" style="width: 200px" clearable>
<el-option
v-for="task in tasks"
:key="task.id"
:label="task.name"
:value="task.id"
<AdminLayout title="学习记录" subtitle="学习记录管理">
<div class="study-records">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户..."
style="width: 200px"
clearable
/>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
<el-select v-model="selectedTask" placeholder="选择任务" style="width: 200px" clearable>
<el-option
v-for="task in tasks"
:key="task.id"
:label="task.name"
:value="task.id"
/>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
<el-table :data="records" style="width: 100%">
<el-table-column prop="userName" label="用户" width="120" />
<el-table-column prop="taskName" label="任务名称" min-width="180" />
<el-table-column prop="progress" label="完成进度" width="120">
<template #default="{ row }">
<el-progress :percentage="row.progress" />
</template>
</el-table-column>
<el-table-column prop="duration" label="学习时长" width="120" />
<el-table-column prop="startDate" label="开始时间" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
<el-table :data="records" style="width: 100%">
<el-table-column prop="userName" label="用户" width="120" />
<el-table-column prop="taskName" label="任务名称" min-width="180" />
<el-table-column prop="progress" label="完成进度" width="120">
<template #default="{ row }">
<el-progress :percentage="row.progress" />
</template>
</el-table-column>
<el-table-column prop="duration" label="学习时长" width="120" />
<el-table-column prop="startDate" label="开始时间" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElProgress, ElPagination, ElMessage } from 'element-plus';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'StudyRecordsView'
});
const searchKeyword = ref('');
const selectedTask = ref('');
const dateRange = ref<[Date, Date] | null>(null);

View File

@@ -1,24 +1,26 @@
<template>
<div class="task-management">
<LearningTaskList
v-if="currentView === 'list'"
@add="handleAdd"
@edit="handleEdit"
/>
<LearningTaskAdd
v-else-if="currentView === 'add' || currentView === 'edit'"
:task-i-d="currentTaskId"
@success="handleSuccess"
@cancel="handleCancel"
/>
</div>
<AdminLayout title="任务管理" subtitle="任务管理">
<div class="task-management">
<LearningTaskList
v-if="currentView === 'list'"
@add="handleAdd"
@edit="handleEdit"
/>
<LearningTaskAdd
v-else-if="currentView === 'add' || currentView === 'edit'"
:task-i-d="currentTaskId"
@success="handleSuccess"
@cancel="handleCancel"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { LearningTaskList, LearningTaskAdd } from '@/views/public/task';
import type { LearningTask } from '@/types/study';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'TaskManageView'
});

View File

@@ -1,12 +1,16 @@
<template>
<div class="dept-manage">
<div class="header">
<h2>部门管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增部门
</el-button>
</div>
<AdminLayout
title="系统管理"
subtitle="管理用户、角色、权限、部门等系统信息"
>
<div class="dept-manage">
<div class="header">
<h2>部门管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增部门
</el-button>
</div>
<div class="tree-container">
<el-tree
@@ -163,12 +167,18 @@
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { deptApi } from '@/apis/system/dept';
import { roleApi } from '@/apis/system/role';
import { SysDept, SysRole } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'DeptManageView'
});
import { ref, onMounted, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus, OfficeBuilding } from '@element-plus/icons-vue';
@@ -605,7 +615,9 @@ async function handleNodeDrop(draggingNode: any, dropNode: any, dropType: string
<style scoped lang="scss">
.dept-manage {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
.header {
display: flex;

View File

@@ -1,12 +1,16 @@
<template>
<div class="menu-manage">
<div class="header">
<h2>菜单管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增菜单
</el-button>
</div>
<AdminLayout
title="系统管理"
subtitle="管理用户、角色、权限、部门等系统信息"
>
<div class="menu-manage">
<div class="header">
<h2>菜单管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增菜单
</el-button>
</div>
<div class="tree-container">
<el-tree
@@ -208,12 +212,18 @@
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { menuApi } from '@/apis/system/menu';
import { permissionApi } from '@/apis/system/permission';
import { SysMenu, SysPermission } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'MenuManageView'
});
import { ref, onMounted, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
@@ -692,7 +702,9 @@ async function handleNodeDrop(draggingNode: any, dropNode: any, dropType: string
<style scoped lang="scss">
.menu-manage {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
.header {
display: flex;

View File

@@ -1,12 +1,16 @@
<template>
<div class="module-permission-manage">
<div class="header">
<h2>模块权限管理</h2>
<el-button type="primary" @click="handleAddModule">
<el-icon><Plus /></el-icon>
新增模块
</el-button>
</div>
<AdminLayout
title="系统管理"
subtitle="管理用户、角色、权限、部门等系统信息"
>
<div class="module-permission-manage">
<div class="header">
<h2>模块权限管理</h2>
<el-button type="primary" @click="handleAddModule">
<el-icon><Plus /></el-icon>
新增模块
</el-button>
</div>
<div class="content-wrapper">
<!-- 左侧模块树 -->
@@ -389,7 +393,8 @@
</el-button>
</template>
</el-dialog>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
@@ -401,6 +406,11 @@ import { permissionApi } from '@/apis/system/permission';
import { menuApi } from '@/apis/system/menu';
import { roleApi } from '@/apis/system/role';
import type { SysModule, SysPermission, SysMenu, SysRole } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'ModulePermissionManageView'
});
// 数据状态
const moduleLoading = ref(false);
@@ -873,9 +883,9 @@ onMounted(() => {
<style scoped lang="scss">
.module-permission-manage {
padding: 20px;
background-color: #fff;
border-radius: 4px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
height: calc(100vh - 120px);
display: flex;
flex-direction: column;

View File

@@ -1,12 +1,16 @@
<template>
<div class="role-manage">
<div class="header">
<h2>角色管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增角色
</el-button>
</div>
<AdminLayout
title="系统管理"
subtitle="管理用户、角色、权限、部门等系统信息"
>
<div class="role-manage">
<div class="header">
<h2>角色管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增角色
</el-button>
</div>
<el-table
:data="roleList"
@@ -148,13 +152,19 @@
</el-button>
</template>
</el-dialog>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { roleApi } from '@/apis/system/role';
import { permissionApi } from '@/apis/system/permission';
import { SysRole, SysPermission } from '@/types';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'RoleManageView'
});
import { ref, onMounted, reactive } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
@@ -415,7 +425,9 @@ onMounted(() => {
<style scoped lang="scss">
.role-manage {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
.header {
display: flex;

View File

@@ -1,217 +1,230 @@
<template>
<div class="user-manage">
<div class="header">
<h2>用户管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增用户
</el-button>
</div>
<el-table
:data="userList"
style="width: 100%"
v-loading="loading"
border
stripe
row-key="userID"
>
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column prop="email" label="邮箱" min-width="180" />
<el-table-column prop="phone" label="手机号" min-width="120" />
<el-table-column prop="realName" label="真实姓名" min-width="100" />
<el-table-column prop="nickname" label="昵称" min-width="100" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 0 ? 'success' : 'danger'">
{{ row.status === 0 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creatorName" label="创建人" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
link
>
编辑
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindDeptRole(row)"
link
>
绑定部门角色
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑用户对话框 -->
<el-dialog
v-model="userDialogVisible"
:title="isEdit ? '编辑用户' : '新增用户'"
width="600px"
@close="resetForm"
>
<el-form
ref="userFormRef"
:model="currentUser"
:rules="userFormRules"
label-width="100px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="currentUser.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit">
<el-input
v-model="currentUser.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="currentUser.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="currentUser.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="currentUser.realName" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="currentUser.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="currentUser.status" placeholder="请选择状态">
<el-option label="正常" :value="0" />
<el-option label="禁用" :value="1" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveUser" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
<AdminLayout
title="系统管理"
subtitle="管理用户、角色、权限、部门等系统信息"
>
<div class="user-manage">
<div class="header">
<h2>用户管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增用户
</el-button>
</template>
</el-dialog>
<!-- 绑定部门角色对话框 -->
<el-dialog
v-model="bindDeptRoleDialogVisible"
title="绑定部门角色"
width="800px"
@close="resetBindForm"
>
<div class="bind-info">
<el-alert
:title="`用户:${currentUser.username} (${currentUser.realName})`"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
</div>
<el-table :data="bindList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<el-table
:data="userList"
style="width: 100%"
v-loading="loading"
border
stripe
row-key="userID"
>
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column prop="email" label="邮箱" min-width="180" />
<el-table-column prop="phone" label="手机号" min-width="120" />
<el-table-column prop="realName" label="真实姓名" min-width="100" />
<el-table-column prop="nickname" label="昵称" min-width="100" />
<el-table-column prop="deptName" label="部门" min-width="120" />
<el-table-column prop="roleName" label="角色" min-width="120" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag
:type="isSelected(row) ? 'success' : 'info'"
size="small"
>
{{ isSelected(row) ? '已绑定' : '未绑定' }}
<el-tag :type="row.status === 0 ? 'success' : 'danger'">
{{ row.status === 0 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deptName" label="部门" min-width="150" />
<el-table-column prop="roleName" label="角色" min-width="150" />
<el-table-column label="操作" width="100">
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
:type="isSelected(row) ? 'danger' : 'primary'"
type="primary"
size="small"
@click="toggleSelection(row)"
@click="handleEdit(row)"
link
>
{{ isSelected(row) ? '解绑' : '绑定' }}
编辑
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindDeptRole(row)"
link
>
绑定部门角色
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="bind-stats" style="margin-top: 20px">
<el-alert
:title="`已绑定 ${selectedBindings.length} 个部门角色,未绑定 ${bindList.length - selectedBindings.length} 个部门角色`"
type="info"
:closable="false"
show-icon
<!-- 分页组件 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<template #footer>
<el-button @click="bindDeptRoleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveBindings" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</div>
<!-- 新增/编辑用户对话框 -->
<el-dialog
v-model="userDialogVisible"
:title="isEdit ? '编辑用户' : '新增用户'"
width="600px"
@close="resetForm"
>
<el-form
ref="userFormRef"
:model="currentUser"
:rules="userFormRules"
label-width="100px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="currentUser.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit">
<el-input
v-model="currentUser.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="currentUser.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="currentUser.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="currentUser.realName" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="currentUser.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="currentUser.status">
<el-radio :value="0">正常</el-radio>
<el-radio :value="1">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUserForm" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
<!-- 绑定部门角色对话框 -->
<el-dialog
v-model="bindDialogVisible"
title="绑定部门角色"
width="600px"
@close="resetBindForm"
>
<el-form
ref="bindFormRef"
:model="bindForm"
label-width="100px"
>
<el-form-item label="选择部门">
<el-tree-select
v-model="bindForm.deptId"
:data="deptTree"
check-strictly
:render-after-expand="false"
placeholder="请选择部门"
/>
</el-form-item>
<el-form-item label="选择角色">
<el-select v-model="bindForm.roleIds" multiple placeholder="请选择角色" style="width: 100%">
<el-option
v-for="role in roles"
:key="role.roleID"
:label="role.name"
:value="role.roleID"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="bindDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBindForm" :loading="binding">
确定
</el-button>
</template>
</el-dialog>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { userApi } from '@/apis/system/user';
import { deptApi } from '@/apis/system/dept';
import { roleApi } from '@/apis/system/role';
import type { SysUser, UserDeptRoleVO, UserVO, SysDeptRole} from '@/types';
import { userApi, deptApi, roleApi } from '@/apis/system';
import type { SysUser, SysRole, PageParam, UserVO, UserDeptRoleVO } from '@/types';
import { AdminLayout } from '@/views/admin';
// 扩展部门角色类型,包含绑定状态和显示信息
interface DeptRoleBindItem extends SysDeptRole {
deptName?: string;
roleName?: string;
isBound?: boolean;
}
defineOptions({
name: 'UserManageView'
});
// 响应式数据
const loading = ref(false);
const submitting = ref(false);
const userList = ref<UserVO[]>([]);
const binding = ref(false);
const userList = ref<UserVO[]>([]);
const deptTree = ref<any[]>([]);
const roles = ref<SysRole[]>([]);
// 分页参数
const pageParam = ref<PageParam>({
pageNumber: 1,
pageSize: 10
});
const total = ref(0);
// 对话框控制
const userDialogVisible = ref(false);
const bindDeptRoleDialogVisible = ref(false);
const bindDialogVisible = ref(false);
const isEdit = ref(false);
// 当前操作的用户
const currentUser = ref<SysUser & { realName?: string; nickname?: string }>({});
const userFormRef = ref<FormInstance>();
const bindFormRef = ref<FormInstance>();
// 绑定相关数据
const bindList = ref<DeptRoleBindItem[]>([]);
const selectedBindings = ref<string[]>([]); // 存储选中的绑定项的唯一标识
const currentUser = ref<UserVO & { password?: string }>({
status: 0
});
// 表单验证规则
const userFormRules = {
const bindForm = ref<{
userId?: string;
deptId?: string;
roleIds: string[];
}>({
roleIds: []
});
const userFormRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
@@ -219,34 +232,20 @@ const userFormRules = {
]
};
// 生成唯一标识
function getBindingKey(item: DeptRoleBindItem): string {
return `${item.deptID}_${item.roleID}`;
}
onMounted(() => {
loadUsers();
loadDepts();
loadRoles();
});
// 检查是否已选中
function isSelected(item: DeptRoleBindItem): boolean {
return selectedBindings.value.includes(getBindingKey(item));
}
// 切换选择状态
function toggleSelection(item: DeptRoleBindItem) {
const key = getBindingKey(item);
const index = selectedBindings.value.indexOf(key);
if (index > -1) {
selectedBindings.value.splice(index, 1);
} else {
selectedBindings.value.push(key);
}
}
// 加载用户列表
async function loadUserList() {
async function loadUsers() {
try {
loading.value = true;
const result = await userApi.getUserList({id: ""});
userList.value = result.dataList || [];
const result = await userApi.getUserPage(pageParam.value);
if (result.success) {
userList.value = result.dataList || [];
total.value = result.pageParam?.totalElements || 0;
}
} catch (error) {
console.error('加载用户列表失败:', error);
ElMessage.error('加载用户列表失败');
@@ -255,267 +254,176 @@ async function loadUserList() {
}
}
// 加载部门和角色数据用于名称映射
async function loadDeptAndRoleData() {
async function loadDepts() {
try {
const [deptResult, roleResult] = await Promise.all([
deptApi.getAllDepts(),
roleApi.getAllRoles()
]);
const depts = deptResult.dataList || [];
const roles = roleResult.dataList || [];
const deptMap = new Map(depts.map(d => [d.deptID, d.name || '未知部门']));
const roleMap = new Map(roles.map(r => [r.roleID, r.name || '未知角色']));
return { deptMap, roleMap };
const result = await deptApi.getAllDepts();
if (result.success) {
deptTree.value = result.dataList || [];
}
} catch (error) {
console.error('加载部门角色数据失败:', error);
return { deptMap: new Map(), roleMap: new Map() };
console.error('加载部门列表失败:', error);
}
}
// 加载部门角色绑定列表
async function loadDeptRoleList(): Promise<DeptRoleBindItem[]> {
async function loadRoles() {
try {
const [deptRoleResult, { deptMap, roleMap }] = await Promise.all([
deptApi.getDeptRoleList({id: ""}),
loadDeptAndRoleData()
]);
const deptRoles = deptRoleResult.dataList || [];
return deptRoles.map(item => ({
...item,
deptName: deptMap.get(item.deptID || '') || `部门${item.deptID}`,
roleName: roleMap.get(item.roleID || '') || `角色${item.roleID}`,
isBound: false // 初始状态为未绑定,需要根据实际绑定情况设置
}));
const result = await roleApi.getRoleList({});
if (result.success) {
roles.value = result.dataList || [];
}
} catch (error) {
console.error('加载部门角色列表失败:', error);
return [];
console.error('加载角色列表失败:', error);
}
}
// 新增用户
function handleAdd() {
isEdit.value = false;
currentUser.value = {};
currentUser.value = { status: 0 };
userDialogVisible.value = true;
}
// 编辑用户
function handleEdit(row: UserVO) {
isEdit.value = true;
// 将UserVO转换为SysUser
currentUser.value = {
id: row.id,
username: row.username,
email: row.email,
phone: row.phone,
wechatID: row.wechatID,
status: row.status,
createTime: row.createTime,
updateTime: row.updateTime,
deleteTime: row.deleteTime,
deleted: row.deleted,
realName: row.realName,
nickname: row.nickname
};
currentUser.value = { ...row };
userDialogVisible.value = true;
}
// 删除用户
async function handleDelete(row: UserVO) {
try {
await ElMessageBox.confirm(
`确定要删除用户 "${row.username}" 吗?`,
'确认删除',
`确定要删除用户"${row.username}"吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}
);
await userApi.deleteUser(row);
ElMessage.success('删除成功');
await loadUserList();
} catch (error) {
const result = await userApi.deleteUser({ id: row.id } as SysUser);
if (result.success) {
ElMessage.success('删除成功');
loadUsers();
} else {
ElMessage.error(result.message || '删除失败');
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除用户失败:', error);
ElMessage.error('删除用户失败');
ElMessage.error('删除失败');
}
}
}
// 保存用户
async function saveUser() {
try {
submitting.value = true;
if (isEdit.value) {
await userApi.updateUser(currentUser.value);
ElMessage.success('更新成功');
} else {
await userApi.createUser(currentUser.value);
ElMessage.success('创建成功');
}
userDialogVisible.value = false;
await loadUserList();
} catch (error) {
console.error('保存用户失败:', error);
ElMessage.error('保存用户失败');
} finally {
submitting.value = false;
}
}
// 绑定部门角色
async function handleBindDeptRole(row: UserVO) {
// 将UserVO转换为SysUser
currentUser.value = {
id: row.id,
username: row.username,
email: row.email,
phone: row.phone,
wechatID: row.wechatID,
status: row.status,
createTime: row.createTime,
updateTime: row.updateTime,
deleteTime: row.deleteTime,
deleted: row.deleted
function handleBindDeptRole(row: UserVO) {
bindForm.value = {
userId: row.id,
deptId: undefined,
roleIds: []
};
try {
// 加载全部部门和角色数据
bindList.value = await loadDeptRoleList();
// 加载已绑定部门角色
const deptRoleResult = await userApi.getUserDeptRole({userID: currentUser.value.id});
const deptRoles = deptRoleResult.dataList || [];
// 初始化选中状态为已绑定的项
selectedBindings.value = bindList.value
.filter(item => deptRoles.some(deptRole =>
deptRole.deptID === item.deptID && deptRole.roleID === item.roleID
))
.map(item => getBindingKey(item));
bindDeptRoleDialogVisible.value = true;
} catch (error) {
console.error('加载绑定数据失败:', error);
ElMessage.error('加载绑定数据失败');
}
bindDialogVisible.value = true;
}
// 保存绑定设置
async function saveBindings() {
if (!currentUser.value || !currentUser.value.id) {
ElMessage.error('用户信息不完整');
return;
}
async function submitUserForm() {
if (!userFormRef.value) return;
try {
await userFormRef.value.validate();
submitting.value = true;
// 获取当前已绑定的部门角色
const currentBoundResult = await userApi.getUserDeptRole({userID: currentUser.value.id});
const currentBoundItems = currentBoundResult.dataList || [];
const currentBoundKeys = currentBoundItems.map(item => `${item.deptID}_${item.roleID}`);
// 找出需要绑定的项(新增的)
const itemsToBind = bindList.value.filter(item => {
const key = getBindingKey(item);
return selectedBindings.value.includes(key) && !currentBoundKeys.includes(key);
});
// 找出需要解绑的项(移除的)
const itemsToUnbind = bindList.value.filter(item => {
const key = getBindingKey(item);
return !selectedBindings.value.includes(key) && currentBoundKeys.includes(key);
});
// 执行绑定操作 - 一次性发送所有绑定项
if (itemsToBind.length > 0) {
const userDeptRoleVO: UserDeptRoleVO = {
user: currentUser.value,
users: [currentUser.value],
userDeptRoles: itemsToBind.map(item => ({
deptID: item.deptID,
roleID: item.roleID
}))
};
await userApi.bindUserDeptRole(userDeptRoleVO);
let result;
if (isEdit.value) {
result = await userApi.updateUser(currentUser.value as SysUser);
} else {
result = await userApi.createUser(currentUser.value as SysUser);
}
// 执行解绑操作 - 一次性发送所有解绑项
if (itemsToUnbind.length > 0) {
const userDeptRoleVO: UserDeptRoleVO = {
user: currentUser.value,
users: [currentUser.value],
userDeptRoles: itemsToUnbind.map(item => ({
deptID: item.deptID,
roleID: item.roleID
}))
};
await userApi.unbindUserDeptRole(userDeptRoleVO);
if (result.success) {
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
userDialogVisible.value = false;
loadUsers();
} else {
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
}
ElMessage.success('部门角色绑定保存成功');
bindDeptRoleDialogVisible.value = false;
// 刷新用户列表
await loadUserList();
} catch (error) {
console.error('保存部门角色绑定失败:', error);
ElMessage.error('保存部门角色绑定失败');
console.error('提交失败:', error);
} finally {
submitting.value = false;
}
}
// 重置表单
async function submitBindForm() {
try {
binding.value = true;
// 构建 UserDeptRoleVO 对象
const userDeptRoleVO: UserDeptRoleVO = {
user: { id: bindForm.value.userId } as SysUser,
depts: bindForm.value.deptId ? [{ id: bindForm.value.deptId }] : [],
roles: bindForm.value.roleIds.map(roleId => ({ id: roleId }))
};
const result = await userApi.bindUserDeptRole(userDeptRoleVO);
if (result.success) {
ElMessage.success('绑定成功');
bindDialogVisible.value = false;
loadUsers();
} else {
ElMessage.error(result.message || '绑定失败');
}
} catch (error) {
console.error('绑定失败:', error);
ElMessage.error('绑定失败');
} finally {
binding.value = false;
}
}
function resetForm() {
currentUser.value = {};
userFormRef.value?.resetFields();
}
// 重置绑定表单
function resetBindForm() {
bindList.value = [];
selectedBindings.value = [];
bindFormRef.value?.resetFields();
}
// 组件挂载时加载数据
onMounted(() => {
loadUserList();
});
function handlePageChange(page: number) {
pageParam.value.pageNumber = page;
loadUsers();
}
function handleSizeChange(size: number) {
pageParam.value.pageSize = size;
pageParam.value.pageNumber = 1; // 改变每页数量时,重置到第一页
loadUsers();
}
</script>
<style scoped>
<style lang="scss" scoped>
.user-manage {
padding: 20px;
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #101828;
}
}
.header h2 {
margin: 0;
}
.bind-info {
margin-bottom: 20px;
}
.bind-stats {
margin-top: 20px;
.pagination-container {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
</style>