视图修改、接口修改
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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点、12点、18点执行)<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点、12点、18点执行)<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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user