数据同步
This commit is contained in:
30
Moncter/TaskShow/src/App.vue
Normal file
30
Moncter/TaskShow/src/App.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// App 根组件
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化 token
|
||||
userStore.initToken()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
115
Moncter/TaskShow/src/api/dataCollection.ts
Normal file
115
Moncter/TaskShow/src/api/dataCollection.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { DataCollectionTask, DataSource } from '@/types'
|
||||
|
||||
// 获取数据采集任务列表
|
||||
export const getDataCollectionTaskList = (params: {
|
||||
name?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
tasks: DataCollectionTask[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/data-collection-tasks', params)
|
||||
}
|
||||
|
||||
// 获取数据采集任务详情
|
||||
export const getDataCollectionTaskDetail = (taskId: string) => {
|
||||
return request.get<DataCollectionTask>(`/data-collection-tasks/${taskId}`)
|
||||
}
|
||||
|
||||
// 创建数据采集任务
|
||||
export const createDataCollectionTask = (data: Partial<DataCollectionTask>) => {
|
||||
return request.post<DataCollectionTask>('/data-collection-tasks', data)
|
||||
}
|
||||
|
||||
// 更新数据采集任务
|
||||
export const updateDataCollectionTask = (taskId: string, data: Partial<DataCollectionTask>) => {
|
||||
return request.put<DataCollectionTask>(`/data-collection-tasks/${taskId}`, data)
|
||||
}
|
||||
|
||||
// 删除数据采集任务
|
||||
export const deleteDataCollectionTask = (taskId: string) => {
|
||||
return request.delete(`/data-collection-tasks/${taskId}`)
|
||||
}
|
||||
|
||||
// 启动数据采集任务
|
||||
export const startDataCollectionTask = (taskId: string) => {
|
||||
return request.post(`/data-collection-tasks/${taskId}/start`)
|
||||
}
|
||||
|
||||
// 暂停数据采集任务
|
||||
export const pauseDataCollectionTask = (taskId: string) => {
|
||||
return request.post(`/data-collection-tasks/${taskId}/pause`)
|
||||
}
|
||||
|
||||
// 停止数据采集任务
|
||||
export const stopDataCollectionTask = (taskId: string) => {
|
||||
return request.post(`/data-collection-tasks/${taskId}/stop`)
|
||||
}
|
||||
|
||||
// 获取任务进度
|
||||
export const getDataCollectionTaskProgress = (taskId: string) => {
|
||||
return request.get(`/data-collection-tasks/${taskId}/progress`)
|
||||
}
|
||||
|
||||
// 获取数据源列表
|
||||
export const getDataSources = () => {
|
||||
return request.get<DataSource[]>('/data-collection-tasks/data-sources')
|
||||
}
|
||||
|
||||
// 获取数据库列表
|
||||
export const getDatabases = (dataSourceId: string) => {
|
||||
return request.get<Array<{ name: string; id: string }>>(`/data-collection-tasks/data-sources/${dataSourceId}/databases`)
|
||||
}
|
||||
|
||||
// 获取集合列表
|
||||
export const getCollections = (dataSourceId: string, database: string | { name: string; id: string }) => {
|
||||
// 如果传入的是对象(包含id),使用id;否则使用字符串(向后兼容)
|
||||
const dbIdentifier = typeof database === 'object' ? database.id : database
|
||||
return request.get<Array<{ name: string; id: string }>>(
|
||||
`/data-collection-tasks/data-sources/${dataSourceId}/databases/${dbIdentifier}/collections`
|
||||
)
|
||||
}
|
||||
|
||||
// 获取字段列表
|
||||
export const getFields = (dataSourceId: string, database: string | { name: string; id: string }, collection: string | { name: string; id: string }) => {
|
||||
// 如果传入的是对象(包含id),使用id;否则使用字符串(向后兼容)
|
||||
const dbIdentifier = typeof database === 'object' ? database.id : database
|
||||
const collIdentifier = typeof collection === 'object' ? collection.id : collection
|
||||
return request.get<Array<{ name: string; type: string }>>(
|
||||
`/data-collection-tasks/data-sources/${dataSourceId}/databases/${dbIdentifier}/collections/${collIdentifier}/fields`
|
||||
)
|
||||
}
|
||||
|
||||
// 获取Handler的目标字段列表
|
||||
export const getHandlerTargetFields = (handlerType: string) => {
|
||||
return request.get<Array<{
|
||||
name: string
|
||||
label: string
|
||||
type: string
|
||||
required: boolean
|
||||
description?: string
|
||||
}>>(`/data-collection-tasks/handlers/${handlerType}/target-fields`)
|
||||
}
|
||||
|
||||
// 预览查询结果
|
||||
export const previewQuery = (data: {
|
||||
data_source_id: string
|
||||
database: string // 这里使用原始名称,因为后端会解码
|
||||
collection: string // 这里使用原始名称,因为后端会解码
|
||||
lookups?: any[]
|
||||
filter_conditions?: any[]
|
||||
limit?: number
|
||||
}) => {
|
||||
return request.post<{
|
||||
fields: Array<{ name: string; type: string }>
|
||||
data: Array<any>
|
||||
count: number
|
||||
}>('/data-collection-tasks/preview-query', data)
|
||||
}
|
||||
|
||||
72
Moncter/TaskShow/src/api/dataSource.ts
Normal file
72
Moncter/TaskShow/src/api/dataSource.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
// 数据源类型
|
||||
export interface DataSource {
|
||||
data_source_id: string
|
||||
name: string
|
||||
type: 'mongodb' | 'mysql' | 'postgresql'
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
username?: string
|
||||
password?: string
|
||||
auth_source?: string
|
||||
options?: Record<string, any>
|
||||
description?: string
|
||||
is_tag_engine?: boolean // 是否为标签引擎数据库(ckb数据库)
|
||||
status: number // 1:启用, 0:禁用
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 获取数据源列表
|
||||
export const getDataSourceList = (params?: {
|
||||
type?: string
|
||||
status?: number
|
||||
name?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
data_sources: DataSource[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/data-sources', params)
|
||||
}
|
||||
|
||||
// 获取数据源详情
|
||||
export const getDataSourceDetail = (dataSourceId: string) => {
|
||||
return request.get<DataSource>(`/data-sources/${dataSourceId}`)
|
||||
}
|
||||
|
||||
// 创建数据源
|
||||
export const createDataSource = (data: Partial<DataSource>) => {
|
||||
return request.post<DataSource>('/data-sources', data)
|
||||
}
|
||||
|
||||
// 更新数据源
|
||||
export const updateDataSource = (dataSourceId: string, data: Partial<DataSource>) => {
|
||||
return request.put<DataSource>(`/data-sources/${dataSourceId}`, data)
|
||||
}
|
||||
|
||||
// 删除数据源
|
||||
export const deleteDataSource = (dataSourceId: string) => {
|
||||
return request.delete(`/data-sources/${dataSourceId}`)
|
||||
}
|
||||
|
||||
// 测试数据源连接
|
||||
export const testDataSourceConnection = (data: {
|
||||
type: string
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
username?: string
|
||||
password?: string
|
||||
auth_source?: string
|
||||
options?: Record<string, any>
|
||||
}) => {
|
||||
return request.post<{ connected: boolean }>('/data-sources/test-connection', data)
|
||||
}
|
||||
|
||||
35
Moncter/TaskShow/src/api/example.ts
Normal file
35
Moncter/TaskShow/src/api/example.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse, PageParams, PageResponse } from '@/types/api'
|
||||
|
||||
// 示例:用户相关 API
|
||||
export interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserList = (params: PageParams) => {
|
||||
return request.get<PageResponse<User>>('/users', params)
|
||||
}
|
||||
|
||||
// 获取用户详情
|
||||
export const getUserDetail = (id: number) => {
|
||||
return request.get<User>(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export const createUser = (data: Omit<User, 'id'>) => {
|
||||
return request.post<User>('/users', data)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export const updateUser = (id: number, data: Partial<User>) => {
|
||||
return request.put<User>(`/users/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id: number) => {
|
||||
return request.delete(`/users/${id}`)
|
||||
}
|
||||
50
Moncter/TaskShow/src/api/tagCohort.ts
Normal file
50
Moncter/TaskShow/src/api/tagCohort.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { TagCohort, TagCohortListItem, TagCondition } from '@/types'
|
||||
|
||||
// 获取人群快照列表
|
||||
export const getTagCohortList = (params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
cohorts: TagCohortListItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/tag-cohorts', params)
|
||||
}
|
||||
|
||||
// 获取人群快照详情
|
||||
export const getTagCohortDetail = (cohortId: string) => {
|
||||
return request.get<TagCohort>(`/tag-cohorts/${cohortId}`)
|
||||
}
|
||||
|
||||
// 创建人群快照
|
||||
export const createTagCohort = (data: {
|
||||
name: string
|
||||
description?: string
|
||||
conditions: TagCondition[]
|
||||
logic?: 'AND' | 'OR'
|
||||
user_ids?: string[]
|
||||
created_by?: string
|
||||
}) => {
|
||||
return request.post<{
|
||||
cohort_id: string
|
||||
name: string
|
||||
user_count: number
|
||||
}>('/tag-cohorts', data)
|
||||
}
|
||||
|
||||
// 删除人群快照
|
||||
export const deleteTagCohort = (cohortId: string) => {
|
||||
return request.delete(`/tag-cohorts/${cohortId}`)
|
||||
}
|
||||
|
||||
// 导出人群快照
|
||||
export const exportTagCohort = (cohortId: string) => {
|
||||
return request.post(`/tag-cohorts/${cohortId}/export`, {}, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
47
Moncter/TaskShow/src/api/tagDefinition.ts
Normal file
47
Moncter/TaskShow/src/api/tagDefinition.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { TagDefinition } from '@/types'
|
||||
|
||||
// 获取标签定义列表
|
||||
export const getTagDefinitionList = (params?: {
|
||||
name?: string
|
||||
category?: string
|
||||
status?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
definitions: TagDefinition[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/tag-definitions', params)
|
||||
}
|
||||
|
||||
// 获取标签定义详情
|
||||
export const getTagDefinitionDetail = (tagId: string) => {
|
||||
return request.get<TagDefinition>(`/tag-definitions/${tagId}`)
|
||||
}
|
||||
|
||||
// 创建标签定义
|
||||
export const createTagDefinition = (data: Partial<TagDefinition>) => {
|
||||
return request.post<TagDefinition>('/tag-definitions', data)
|
||||
}
|
||||
|
||||
// 更新标签定义
|
||||
export const updateTagDefinition = (tagId: string, data: Partial<TagDefinition>) => {
|
||||
return request.put<TagDefinition>(`/tag-definitions/${tagId}`, data)
|
||||
}
|
||||
|
||||
// 删除标签定义
|
||||
export const deleteTagDefinition = (tagId: string) => {
|
||||
return request.delete(`/tag-definitions/${tagId}`)
|
||||
}
|
||||
|
||||
// 批量初始化标签定义
|
||||
export const batchInitTagDefinitions = (data: {
|
||||
definitions: Partial<TagDefinition>[]
|
||||
}) => {
|
||||
return request.post('/tag-definitions/batch', data)
|
||||
}
|
||||
|
||||
64
Moncter/TaskShow/src/api/tagQuery.ts
Normal file
64
Moncter/TaskShow/src/api/tagQuery.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { UserTag, UserInfo, TagCondition, TagStatistics, TagHistoryResponse } from '@/types'
|
||||
|
||||
// 获取用户标签(支持通过用户ID或手机号查询)
|
||||
export const getUserTags = (userIdOrPhone: string) => {
|
||||
return request.get<{
|
||||
user_id: string
|
||||
tags: UserTag[]
|
||||
count: number
|
||||
}>(`/users/${userIdOrPhone}/tags`)
|
||||
}
|
||||
|
||||
// 重新计算用户标签
|
||||
export const recalculateUserTags = (userId: string) => {
|
||||
return request.put<{
|
||||
user_id: string
|
||||
updated_tags: Array<{
|
||||
tag_id: string
|
||||
tag_code: string
|
||||
tag_value: string
|
||||
}>
|
||||
count: number
|
||||
}>(`/users/${userId}/tags`)
|
||||
}
|
||||
|
||||
// 根据标签筛选用户
|
||||
export const filterUsersByTags = (params: {
|
||||
tag_conditions: TagCondition[]
|
||||
logic: 'AND' | 'OR'
|
||||
page?: number
|
||||
page_size?: number
|
||||
include_user_info?: boolean
|
||||
}) => {
|
||||
return request.post<{
|
||||
users: UserInfo[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
total_pages?: number
|
||||
}>('/tags/filter', params)
|
||||
}
|
||||
|
||||
// 获取标签统计信息
|
||||
export const getTagStatistics = (params?: {
|
||||
tag_id?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}) => {
|
||||
return request.get<TagStatistics>('/tags/statistics', params)
|
||||
}
|
||||
|
||||
// 获取标签历史记录
|
||||
export const getTagHistory = (params?: {
|
||||
user_id?: string
|
||||
tag_id?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<TagHistoryResponse>('/tags/history', params)
|
||||
}
|
||||
|
||||
66
Moncter/TaskShow/src/api/tagTask.ts
Normal file
66
Moncter/TaskShow/src/api/tagTask.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { TagTask, TaskExecution } from '@/types'
|
||||
|
||||
// 获取标签任务列表
|
||||
export const getTagTaskList = (params: {
|
||||
name?: string
|
||||
task_type?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
tasks: TagTask[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/tag-tasks', params)
|
||||
}
|
||||
|
||||
// 获取标签任务详情
|
||||
export const getTagTaskDetail = (taskId: string) => {
|
||||
return request.get<TagTask>(`/tag-tasks/${taskId}`)
|
||||
}
|
||||
|
||||
// 创建标签任务
|
||||
export const createTagTask = (data: Partial<TagTask>) => {
|
||||
return request.post<TagTask>('/tag-tasks', data)
|
||||
}
|
||||
|
||||
// 更新标签任务
|
||||
export const updateTagTask = (taskId: string, data: Partial<TagTask>) => {
|
||||
return request.put<TagTask>(`/tag-tasks/${taskId}`, data)
|
||||
}
|
||||
|
||||
// 删除标签任务
|
||||
export const deleteTagTask = (taskId: string) => {
|
||||
return request.delete(`/tag-tasks/${taskId}`)
|
||||
}
|
||||
|
||||
// 启动标签任务
|
||||
export const startTagTask = (taskId: string) => {
|
||||
return request.post(`/tag-tasks/${taskId}/start`)
|
||||
}
|
||||
|
||||
// 暂停标签任务
|
||||
export const pauseTagTask = (taskId: string) => {
|
||||
return request.post(`/tag-tasks/${taskId}/pause`)
|
||||
}
|
||||
|
||||
// 停止标签任务
|
||||
export const stopTagTask = (taskId: string) => {
|
||||
return request.post(`/tag-tasks/${taskId}/stop`)
|
||||
}
|
||||
|
||||
// 获取任务执行记录
|
||||
export const getTagTaskExecutions = (taskId: string, params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.get<{
|
||||
executions: TaskExecution[]
|
||||
total: number
|
||||
}>(`/tag-tasks/${taskId}/executions`, params)
|
||||
}
|
||||
|
||||
33
Moncter/TaskShow/src/api/user.ts
Normal file
33
Moncter/TaskShow/src/api/user.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { UserInfo } from '@/types'
|
||||
|
||||
// 搜索用户(复杂查询)
|
||||
export const searchUsers = (params: {
|
||||
id_card?: string
|
||||
phone?: string
|
||||
name?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
return request.post<{
|
||||
users: UserInfo[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}>('/users/search', params)
|
||||
}
|
||||
|
||||
// 解密身份证
|
||||
export const decryptIdCard = (userId: string) => {
|
||||
return request.get<{
|
||||
user_id: string
|
||||
id_card: string
|
||||
}>(`/users/${userId}/decrypt-id-card`)
|
||||
}
|
||||
|
||||
// 删除用户标签
|
||||
export const deleteUserTag = (userId: string, tagId: string) => {
|
||||
return request.delete(`/users/${userId}/tags/${tagId}`)
|
||||
}
|
||||
|
||||
8
Moncter/TaskShow/src/assets/README.md
Normal file
8
Moncter/TaskShow/src/assets/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Assets 目录
|
||||
|
||||
存放静态资源文件,如:
|
||||
- 图片
|
||||
- 样式文件
|
||||
- 字体文件
|
||||
- 其他静态资源
|
||||
|
||||
178
Moncter/TaskShow/src/components/Layout/index.vue
Normal file
178
Moncter/TaskShow/src/components/Layout/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
|
||||
<div class="logo">
|
||||
<h2 v-if="!isCollapse">TaskShow</h2>
|
||||
<h2 v-else>T</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409eff"
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<template #title>首页</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="data-collection">
|
||||
<template #title>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>数据采集</span>
|
||||
</template>
|
||||
<el-menu-item index="/data-collection/tasks">任务列表</el-menu-item>
|
||||
<el-menu-item index="/data-sources">数据源配置</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="tag-tasks">
|
||||
<template #title>
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
<span>标签任务</span>
|
||||
</template>
|
||||
<el-menu-item index="/tag-tasks">任务列表</el-menu-item>
|
||||
<el-menu-item index="/tag-definitions">标签定义</el-menu-item>
|
||||
<el-menu-item index="/tag-data-lists">数据列表管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item index="/tag-filter">
|
||||
<el-icon><Filter /></el-icon>
|
||||
<template #title>标签筛选</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="tag-query">
|
||||
<template #title>
|
||||
<el-icon><Search /></el-icon>
|
||||
<span>标签查询</span>
|
||||
</template>
|
||||
<el-menu-item index="/tag-query/user">用户标签</el-menu-item>
|
||||
<el-menu-item index="/tag-query/statistics">标签统计</el-menu-item>
|
||||
<el-menu-item index="/tag-query/history">标签历史</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
:icon="isCollapse ? Expand : Fold"
|
||||
circle
|
||||
@click="toggleCollapse"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown>
|
||||
<span class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>管理员</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>个人设置</el-dropdown-item>
|
||||
<el-dropdown-item divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
HomeFilled,
|
||||
Document,
|
||||
PriceTag,
|
||||
Filter,
|
||||
Search,
|
||||
User,
|
||||
Fold,
|
||||
Expand
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const isCollapse = ref(false)
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #304156;
|
||||
transition: width 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
background-color: #2b3a4a;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
139
Moncter/TaskShow/src/components/ProgressDisplay/index.vue
Normal file
139
Moncter/TaskShow/src/components/ProgressDisplay/index.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="progress-display">
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">{{ title }}</span>
|
||||
<span class="progress-percentage">{{ percentage }}%</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="percentage"
|
||||
:status="progressStatus"
|
||||
:stroke-width="strokeWidth"
|
||||
/>
|
||||
<div class="progress-stats" v-if="showStats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总数:</span>
|
||||
<span class="stat-value">{{ total }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已处理:</span>
|
||||
<span class="stat-value">{{ processed }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">成功:</span>
|
||||
<span class="stat-value success">{{ success }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">失败:</span>
|
||||
<span class="stat-value error">{{ error }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="progress-time" v-if="showTime">
|
||||
<span v-if="startTime">开始时间:{{ formatTime(startTime) }}</span>
|
||||
<span v-if="endTime" style="margin-left: 20px">结束时间:{{ formatTime(endTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
percentage: number
|
||||
total?: number
|
||||
processed?: number
|
||||
success?: number
|
||||
error?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
showStats?: boolean
|
||||
showTime?: boolean
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '进度',
|
||||
percentage: 0,
|
||||
total: 0,
|
||||
processed: 0,
|
||||
success: 0,
|
||||
error: 0,
|
||||
showStats: true,
|
||||
showTime: true,
|
||||
strokeWidth: 8
|
||||
})
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
if (props.percentage === 100) return 'success'
|
||||
if (props.error > 0 && props.processed === props.total) return 'exception'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.progress-display {
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.progress-title {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
margin-top: 15px;
|
||||
|
||||
.stat-item {
|
||||
.stat-label {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
&.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-time {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1092
Moncter/TaskShow/src/components/QueryBuilder/QueryBuilder.vue
Normal file
1092
Moncter/TaskShow/src/components/QueryBuilder/QueryBuilder.vue
Normal file
File diff suppressed because it is too large
Load Diff
294
Moncter/TaskShow/src/components/QueryBuilder/README.md
Normal file
294
Moncter/TaskShow/src/components/QueryBuilder/README.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# QueryBuilder 组件说明
|
||||
|
||||
可视化查询构建器组件,支持 MongoDB 聚合查询配置。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### ✅ 基础配置
|
||||
- 数据源选择
|
||||
- 数据库选择
|
||||
- 单集合/多集合模式切换
|
||||
|
||||
### ✅ 多集合模式(新增)
|
||||
- **集合列表展示**:复选框多选
|
||||
- **文本筛选**:实时搜索过滤集合
|
||||
- **批量操作**:
|
||||
- 全选:选择当前筛选结果的所有集合
|
||||
- 清空:清空所有已选集合
|
||||
- 反选:反选当前筛选结果
|
||||
- **智能快捷筛选**(自动识别日期分片集合):
|
||||
- 按年份:2021年、2022年、2023年、2024年、2025年
|
||||
- 按时间范围:最近3个月、最近6个月、最近12个月
|
||||
|
||||
### ✅ 过滤条件(WHERE)
|
||||
- 多条件支持
|
||||
- AND/OR 逻辑关系
|
||||
- 丰富的运算符:
|
||||
- 等于、不等于
|
||||
- 大于、大于等于、小于、小于等于
|
||||
- 包含、不包含
|
||||
- 模糊匹配、存在
|
||||
|
||||
### ✅ 联表查询(JOIN/LOOKUP)
|
||||
- 多表关联
|
||||
- 支持 LEFT JOIN 效果
|
||||
- 数组解构选项
|
||||
|
||||
### ✅ 排序和限制
|
||||
- 字段排序
|
||||
- 升序/降序
|
||||
- 结果数量限制
|
||||
|
||||
### ✅ 查询预览
|
||||
- 实时生成 MongoDB 聚合管道代码
|
||||
- 数据预览(最多显示配置的 limit 条)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<QueryBuilder v-model="queryConfig" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import QueryBuilder from '@/components/QueryBuilder/QueryBuilder.vue'
|
||||
|
||||
const queryConfig = reactive({
|
||||
data_source_id: '',
|
||||
database: '',
|
||||
collection: '',
|
||||
multi_collection: false,
|
||||
collections: [],
|
||||
filter: [],
|
||||
lookups: [],
|
||||
sort_field: '',
|
||||
sort_order: '1',
|
||||
limit: 1000
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多集合模式示例
|
||||
|
||||
```javascript
|
||||
// 配置数据结构
|
||||
{
|
||||
data_source_id: 'source_001',
|
||||
database: 'ckb',
|
||||
collection: 'consumption_records_202101', // 第一个集合(兼容性)
|
||||
multi_collection: true, // 启用多集合模式
|
||||
collections: [ // 选中的集合列表
|
||||
'consumption_records_202101',
|
||||
'consumption_records_202102',
|
||||
'consumption_records_202103',
|
||||
'consumption_records_202104',
|
||||
'consumption_records_202105',
|
||||
'consumption_records_202106'
|
||||
],
|
||||
filter: [
|
||||
{ field: 'status', operator: 'eq', value: 'success' }
|
||||
],
|
||||
lookups: [],
|
||||
sort_field: 'create_time',
|
||||
sort_order: '-1',
|
||||
limit: 1000
|
||||
}
|
||||
```
|
||||
|
||||
## 界面布局
|
||||
|
||||
```
|
||||
┌─ 基础配置 ─────────────────────────────────────────┐
|
||||
│ 数据源:[MongoDB数据源 ▼] │
|
||||
│ 数据库:[ckb ▼] │
|
||||
│ 多集合模式:[●启用] ○禁用 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 集合列表 ─────────────────────────────────────────┐
|
||||
│ [🔍 筛选集合名称...] [全选] [清空] [反选] │
|
||||
│ │
|
||||
│ 快捷筛选:[2021年] [2022年] [2023年] [2024年] │
|
||||
│ [最近3个月] [最近6个月] [最近12个月] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ ☑ consumption_records_202101 │
|
||||
│ ☑ consumption_records_202102 │
|
||||
│ ☑ consumption_records_202103 │
|
||||
│ ☐ consumption_records_202104 │
|
||||
│ ☐ consumption_records_202105 │
|
||||
│ ... (滚动查看更多) │
|
||||
│ │
|
||||
│ 筛选结果:28个集合 | 已选择 3个集合 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 过滤条件(WHERE) ──────────────── [添加条件] ───┐
|
||||
│ 逻辑 | 字段 | 运算符 | 值 | 操作 │
|
||||
│ - | status | 等于 | success | [删除] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 联表查询(JOIN) ─────────────── [添加关联] ────┐
|
||||
│ 关联集合 | 主字段 | 关联字段 | 结果名 | 操作 │
|
||||
│ user_profile | user_id | user_id | user_info | [删除] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 排序和限制 ────────────────────────────────────────┐
|
||||
│ 排序字段:[create_time ▼] 排序方式:[降序 ▼] │
|
||||
│ 限制数量:[1000] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 查询预览 ───────────────────────── [预览数据] ───┐
|
||||
│ // 多集合模式:将查询以下 3 个集合并合并结果 │
|
||||
│ // consumption_records_202101, ... │
|
||||
│ │
|
||||
│ db.consumption_records_202101.aggregate([ │
|
||||
│ { $match: { status: { $eq: "success" } } }, │
|
||||
│ { $sort: { create_time: -1 } }, │
|
||||
│ { $limit: 1000 } │
|
||||
│ ]) │
|
||||
│ │
|
||||
│ [预览数据表格] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| modelValue | Object | - | 查询配置对象(v-model 绑定) |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| update:modelValue | queryConfig | 配置变化时触发 |
|
||||
|
||||
## 多集合模式特性
|
||||
|
||||
### 智能识别
|
||||
自动检测集合命名格式,如果包含6位数字(YYYYMM格式),则显示快捷筛选按钮。
|
||||
|
||||
**支持的格式**:
|
||||
- `consumption_records_202101`
|
||||
- `collection_2021_01`
|
||||
- `data_202101_backup`
|
||||
|
||||
### 快捷筛选逻辑
|
||||
|
||||
#### 按年份筛选
|
||||
匹配包含年份字符串的集合。
|
||||
|
||||
```javascript
|
||||
// 点击 "2021年"
|
||||
collections.filter(coll => coll.includes('2021'))
|
||||
```
|
||||
|
||||
#### 按时间范围筛选
|
||||
计算当前日期向前推算N个月,匹配包含对应年月的集合。
|
||||
|
||||
```javascript
|
||||
// 点击 "最近3个月"(假设当前 2025-01)
|
||||
匹配:202501, 202412, 202411
|
||||
```
|
||||
|
||||
### 筛选功能实现
|
||||
|
||||
```javascript
|
||||
// 实时筛选
|
||||
const filteredCollections = computed(() => {
|
||||
if (!collectionFilter.value) return collections.value
|
||||
const filter = collectionFilter.value.toLowerCase()
|
||||
return collections.value.filter(coll =>
|
||||
coll.toLowerCase().includes(filter)
|
||||
)
|
||||
})
|
||||
|
||||
// 全选
|
||||
const handleSelectAllCollections = () => {
|
||||
const newSelections = [
|
||||
...new Set([
|
||||
...queryConfig.collections,
|
||||
...filteredCollections.value
|
||||
])
|
||||
]
|
||||
queryConfig.collections = newSelections
|
||||
}
|
||||
|
||||
// 反选
|
||||
const handleInvertCollections = () => {
|
||||
const currentSelections = new Set(queryConfig.collections)
|
||||
const newSelections = filteredCollections.value.filter(
|
||||
coll => !currentSelections.has(coll)
|
||||
)
|
||||
const keptSelections = queryConfig.collections.filter(
|
||||
coll => !filteredCollections.value.includes(coll)
|
||||
)
|
||||
queryConfig.collections = [...keptSelections, ...newSelections]
|
||||
}
|
||||
```
|
||||
|
||||
## API 依赖
|
||||
|
||||
组件依赖以下 API:
|
||||
|
||||
```typescript
|
||||
import * as dataCollectionApi from '@/api/dataCollection'
|
||||
|
||||
// 获取数据源列表
|
||||
dataCollectionApi.getDataSources()
|
||||
|
||||
// 获取数据库列表
|
||||
dataCollectionApi.getDatabases(dataSourceId)
|
||||
|
||||
// 获取集合列表
|
||||
dataCollectionApi.getCollections(dataSourceId, database)
|
||||
|
||||
// 获取字段列表
|
||||
dataCollectionApi.getFields(dataSourceId, database, collection)
|
||||
|
||||
// 预览查询结果
|
||||
dataCollectionApi.previewQuery({
|
||||
data_source_id,
|
||||
database,
|
||||
collection, // 单集合模式
|
||||
collections, // 多集合模式
|
||||
lookups,
|
||||
filter_conditions,
|
||||
limit
|
||||
})
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字段一致性**
|
||||
- 多集合模式下,各集合应有相同或相似的字段结构
|
||||
- 字段列表从第一个选中的集合加载
|
||||
|
||||
2. **性能考虑**
|
||||
- 选择的集合越多,查询性能越慢
|
||||
- 建议根据实际需求选择合适的集合范围
|
||||
- 合理设置 `limit` 限制返回数据量
|
||||
|
||||
3. **后端支持**
|
||||
- 多集合模式需要后端 API 支持 `collections` 参数
|
||||
- 后端需要遍历所有集合执行查询并合并结果
|
||||
- 合并后需要重新应用排序和限制
|
||||
|
||||
4. **数据格式**
|
||||
- API 返回的数据库和集合可能是对象或字符串
|
||||
- 组件内部会自动处理格式转换
|
||||
- 保存时同时存储对象用于后续 API 调用
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [多集合模式使用说明](../../../提示词/多集合模式使用说明.md)
|
||||
- [集合筛选功能使用技巧](../../../提示词/集合筛选功能使用技巧.md)
|
||||
- [数据列表管理界面使用说明](../../../提示词/数据列表管理界面使用说明.md)
|
||||
- [真实API接入说明](../../../提示词/数据列表管理_真实API接入说明.md)
|
||||
|
||||
---
|
||||
|
||||
**版本**:v2.0
|
||||
**更新时间**:2025-01-XX
|
||||
**作者**:CKB Team
|
||||
18
Moncter/TaskShow/src/components/README.md
Normal file
18
Moncter/TaskShow/src/components/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Components 目录
|
||||
|
||||
存放公共组件文件。
|
||||
|
||||
## 使用示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 使用公共组件 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 组件逻辑
|
||||
</script>
|
||||
```
|
||||
|
||||
48
Moncter/TaskShow/src/components/StatusBadge/index.vue
Normal file
48
Moncter/TaskShow/src/components/StatusBadge/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<el-tag :type="tagType" :effect="effect">
|
||||
{{ label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
status: string
|
||||
effect?: 'dark' | 'light' | 'plain'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
effect: 'light'
|
||||
})
|
||||
|
||||
const statusMap: Record<string, { type: string; label: string }> = {
|
||||
// 任务状态
|
||||
pending: { type: 'info', label: '待启动' },
|
||||
running: { type: 'success', label: '运行中' },
|
||||
paused: { type: 'warning', label: '已暂停' },
|
||||
stopped: { type: 'info', label: '已停止' },
|
||||
completed: { type: 'success', label: '已完成' },
|
||||
error: { type: 'danger', label: '错误' },
|
||||
|
||||
// 进度状态
|
||||
idle: { type: 'info', label: '空闲' },
|
||||
|
||||
// 执行状态
|
||||
failed: { type: 'danger', label: '失败' },
|
||||
cancelled: { type: 'info', label: '已取消' },
|
||||
|
||||
// 标签状态
|
||||
active: { type: 'success', label: '启用' },
|
||||
inactive: { type: 'info', label: '禁用' },
|
||||
}
|
||||
|
||||
const tagType = computed(() => {
|
||||
return statusMap[props.status]?.type || 'info'
|
||||
})
|
||||
|
||||
const label = computed(() => {
|
||||
return statusMap[props.status]?.label || props.status
|
||||
})
|
||||
</script>
|
||||
|
||||
22
Moncter/TaskShow/src/main.ts
Normal file
22
Moncter/TaskShow/src/main.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册所有 Element Plus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
199
Moncter/TaskShow/src/router/index.ts
Normal file
199
Moncter/TaskShow/src/router/index.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
import Layout from '@/components/Layout/index.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard/index.vue'),
|
||||
meta: { title: '首页' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/data-collection',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'tasks',
|
||||
name: 'DataCollectionTaskList',
|
||||
component: () => import('@/views/DataCollection/TaskList.vue'),
|
||||
meta: { title: '数据采集任务列表' }
|
||||
},
|
||||
{
|
||||
path: 'tasks/create',
|
||||
name: 'DataCollectionTaskCreate',
|
||||
component: () => import('@/views/DataCollection/TaskForm.vue'),
|
||||
meta: { title: '创建数据采集任务' }
|
||||
},
|
||||
{
|
||||
path: 'tasks/:id',
|
||||
name: 'DataCollectionTaskDetail',
|
||||
component: () => import('@/views/DataCollection/TaskDetail.vue'),
|
||||
meta: { title: '数据采集任务详情' }
|
||||
},
|
||||
{
|
||||
path: 'tasks/:id/edit',
|
||||
name: 'DataCollectionTaskEdit',
|
||||
component: () => import('@/views/DataCollection/TaskForm.vue'),
|
||||
meta: { title: '编辑数据采集任务' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tag-tasks',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'TagTaskList',
|
||||
component: () => import('@/views/TagTask/TaskList.vue'),
|
||||
meta: { title: '标签任务列表' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'TagTaskCreate',
|
||||
component: () => import('@/views/TagTask/TaskForm.vue'),
|
||||
meta: { title: '创建标签任务' }
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
name: 'TagTaskDetail',
|
||||
component: () => import('@/views/TagTask/TaskDetail.vue'),
|
||||
meta: { title: '标签任务详情' }
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'TagTaskEdit',
|
||||
component: () => import('@/views/TagTask/TaskForm.vue'),
|
||||
meta: { title: '编辑标签任务' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tag-definitions',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'TagDefinitionList',
|
||||
component: () => import('@/views/TagDefinition/List.vue'),
|
||||
meta: { title: '标签定义列表' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'TagDefinitionCreate',
|
||||
component: () => import('@/views/TagDefinition/Form.vue'),
|
||||
meta: { title: '创建标签定义' }
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
name: 'TagDefinitionDetail',
|
||||
component: () => import('@/views/TagDefinition/Detail.vue'),
|
||||
meta: { title: '标签定义详情' }
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'TagDefinitionEdit',
|
||||
component: () => import('@/views/TagDefinition/Form.vue'),
|
||||
meta: { title: '编辑标签定义' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tag-filter',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'TagFilter',
|
||||
component: () => import('@/views/TagFilter/index.vue'),
|
||||
meta: { title: '标签筛选' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tag-query',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'UserTagQuery',
|
||||
component: () => import('@/views/TagQuery/User.vue'),
|
||||
meta: { title: '用户标签查询' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'TagStatistics',
|
||||
component: () => import('@/views/TagQuery/Statistics.vue'),
|
||||
meta: { title: '标签统计' }
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
name: 'TagHistory',
|
||||
component: () => import('@/views/TagQuery/History.vue'),
|
||||
meta: { title: '标签历史' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tag-data-lists',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'TagDataListList',
|
||||
component: () => import('@/views/TagDataList/List.vue'),
|
||||
meta: { title: '数据列表管理' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'TagDataListCreate',
|
||||
component: () => import('@/views/TagDataList/Form.vue'),
|
||||
meta: { title: '创建数据列表' }
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'TagDataListEdit',
|
||||
component: () => import('@/views/TagDataList/Form.vue'),
|
||||
meta: { title: '编辑数据列表' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/data-sources',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'DataSourceList',
|
||||
component: () => import('@/views/DataSource/List.vue'),
|
||||
meta: { title: '数据源列表' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'DataSourceCreate',
|
||||
component: () => import('@/views/DataSource/Form.vue'),
|
||||
meta: { title: '创建数据源' }
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'DataSourceEdit',
|
||||
component: () => import('@/views/DataSource/Form.vue'),
|
||||
meta: { title: '编辑数据源' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
6
Moncter/TaskShow/src/shims-vue.d.ts
vendored
Normal file
6
Moncter/TaskShow/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
212
Moncter/TaskShow/src/store/dataCollection.ts
Normal file
212
Moncter/TaskShow/src/store/dataCollection.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { DataCollectionTask, DataSource } from '@/types'
|
||||
import * as dataCollectionApi from '@/api/dataCollection'
|
||||
|
||||
export const useDataCollectionStore = defineStore('dataCollection', () => {
|
||||
const tasks = ref<DataCollectionTask[]>([])
|
||||
const currentTask = ref<DataCollectionTask | null>(null)
|
||||
const dataSources = ref<DataSource[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取任务列表
|
||||
const fetchTasks = async (params?: {
|
||||
name?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await dataCollectionApi.getDataCollectionTaskList(params || {})
|
||||
tasks.value = response.data.tasks
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务详情
|
||||
const fetchTaskDetail = async (taskId: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await dataCollectionApi.getDataCollectionTaskDetail(taskId)
|
||||
currentTask.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
const createTask = async (data: Partial<DataCollectionTask>) => {
|
||||
try {
|
||||
const response = await dataCollectionApi.createDataCollectionTask(data)
|
||||
await fetchTasks()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('创建任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
const updateTask = async (taskId: string, data: Partial<DataCollectionTask>) => {
|
||||
try {
|
||||
const response = await dataCollectionApi.updateDataCollectionTask(taskId, data)
|
||||
await fetchTasks()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deleteTask = async (taskId: string, params?: {
|
||||
name?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
try {
|
||||
await dataCollectionApi.deleteDataCollectionTask(taskId)
|
||||
// 刷新列表时保持当前的筛选和分页状态,并返回结果以便更新分页信息
|
||||
return await fetchTasks(params)
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 启动任务
|
||||
const startTask = async (taskId: string) => {
|
||||
try {
|
||||
await dataCollectionApi.startDataCollectionTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('启动任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
const pauseTask = async (taskId: string) => {
|
||||
try {
|
||||
await dataCollectionApi.pauseDataCollectionTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('暂停任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 停止任务
|
||||
const stopTask = async (taskId: string) => {
|
||||
try {
|
||||
await dataCollectionApi.stopDataCollectionTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('停止任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据源列表
|
||||
const fetchDataSources = async () => {
|
||||
try {
|
||||
const response = await dataCollectionApi.getDataSources()
|
||||
dataSources.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取数据源列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务进度
|
||||
const fetchTaskProgress = async (taskId: string) => {
|
||||
try {
|
||||
const response = await dataCollectionApi.getDataCollectionTaskProgress(taskId)
|
||||
if (currentTask.value && currentTask.value.task_id === taskId) {
|
||||
currentTask.value.progress = response.data
|
||||
}
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取任务进度失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 复制任务
|
||||
const duplicateTask = async (taskId: string) => {
|
||||
try {
|
||||
// 获取原任务详情
|
||||
const originalTask = await fetchTaskDetail(taskId)
|
||||
if (!originalTask) {
|
||||
throw new Error('任务不存在')
|
||||
}
|
||||
|
||||
// 复制任务数据,但清除运行时数据
|
||||
const taskData: Partial<DataCollectionTask> = {
|
||||
name: `${originalTask.name}_副本`,
|
||||
description: originalTask.description || '',
|
||||
data_source_id: originalTask.data_source_id,
|
||||
database: originalTask.database,
|
||||
collection: originalTask.collection || null,
|
||||
collections: originalTask.collections || null,
|
||||
target_type: originalTask.target_type || 'generic',
|
||||
target_data_source_id: originalTask.target_data_source_id || null,
|
||||
target_database: originalTask.target_database || null,
|
||||
target_collection: originalTask.target_collection || null,
|
||||
mode: originalTask.mode || 'batch',
|
||||
field_mappings: originalTask.field_mappings || [],
|
||||
collection_field_mappings: originalTask.collection_field_mappings || {},
|
||||
lookups: originalTask.lookups || [],
|
||||
collection_lookups: originalTask.collection_lookups || {},
|
||||
filter_conditions: originalTask.filter_conditions || [],
|
||||
schedule: originalTask.schedule || { enabled: false, cron: '' },
|
||||
status: 'pending' // 复制后的任务状态为待启动
|
||||
}
|
||||
|
||||
// 创建新任务
|
||||
const response = await dataCollectionApi.createDataCollectionTask(taskData)
|
||||
await fetchTasks()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('复制任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置当前任务
|
||||
const resetCurrentTask = () => {
|
||||
currentTask.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
currentTask,
|
||||
dataSources,
|
||||
loading,
|
||||
fetchTasks,
|
||||
fetchTaskDetail,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
startTask,
|
||||
pauseTask,
|
||||
stopTask,
|
||||
fetchDataSources,
|
||||
fetchTaskProgress,
|
||||
duplicateTask,
|
||||
resetCurrentTask
|
||||
}
|
||||
})
|
||||
|
||||
107
Moncter/TaskShow/src/store/dataSource.ts
Normal file
107
Moncter/TaskShow/src/store/dataSource.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import * as dataSourceApi from '@/api/dataSource'
|
||||
import type { DataSource } from '@/api/dataSource'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useDataSourceStore = defineStore('dataSource', () => {
|
||||
const dataSources = ref<DataSource[]>([])
|
||||
const currentDataSource = ref<DataSource | null>(null)
|
||||
|
||||
// 获取数据源列表
|
||||
const fetchDataSources = async (params?: {
|
||||
type?: string
|
||||
status?: number
|
||||
name?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
try {
|
||||
const response = await dataSourceApi.getDataSourceList(params)
|
||||
dataSources.value = response.data.data_sources
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取数据源列表失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据源详情
|
||||
const fetchDataSourceDetail = async (dataSourceId: string) => {
|
||||
try {
|
||||
const response = await dataSourceApi.getDataSourceDetail(dataSourceId)
|
||||
currentDataSource.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取数据源详情失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据源
|
||||
const createDataSource = async (data: Partial<DataSource>) => {
|
||||
try {
|
||||
const response = await dataSourceApi.createDataSource(data)
|
||||
ElMessage.success('数据源创建成功')
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '创建数据源失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新数据源
|
||||
const updateDataSource = async (dataSourceId: string, data: Partial<DataSource>) => {
|
||||
try {
|
||||
await dataSourceApi.updateDataSource(dataSourceId, data)
|
||||
ElMessage.success('数据源更新成功')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '更新数据源失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据源
|
||||
const deleteDataSource = async (dataSourceId: string) => {
|
||||
try {
|
||||
await dataSourceApi.deleteDataSource(dataSourceId)
|
||||
ElMessage.success('数据源删除成功')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '删除数据源失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
const testConnection = async (data: {
|
||||
type: string
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
username?: string
|
||||
password?: string
|
||||
auth_source?: string
|
||||
options?: Record<string, any>
|
||||
}) => {
|
||||
try {
|
||||
const response = await dataSourceApi.testDataSourceConnection(data)
|
||||
// 只返回结果,不显示消息,由调用方决定是否显示
|
||||
return response.data.connected
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '连接测试失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dataSources,
|
||||
currentDataSource,
|
||||
fetchDataSources,
|
||||
fetchDataSourceDetail,
|
||||
createDataSource,
|
||||
updateDataSource,
|
||||
deleteDataSource,
|
||||
testConnection,
|
||||
}
|
||||
})
|
||||
|
||||
6
Moncter/TaskShow/src/store/index.ts
Normal file
6
Moncter/TaskShow/src/store/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Store 统一导出
|
||||
export { useUserStore } from './user'
|
||||
export * from './dataCollection'
|
||||
export * from './tagTask'
|
||||
export * from './tagDefinition'
|
||||
export { useDataSourceStore } from './dataSource'
|
||||
105
Moncter/TaskShow/src/store/tagDefinition.ts
Normal file
105
Moncter/TaskShow/src/store/tagDefinition.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { TagDefinition } from '@/types'
|
||||
import * as tagDefinitionApi from '@/api/tagDefinition'
|
||||
|
||||
export const useTagDefinitionStore = defineStore('tagDefinition', () => {
|
||||
const definitions = ref<TagDefinition[]>([])
|
||||
const currentDefinition = ref<TagDefinition | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取标签定义列表
|
||||
const fetchDefinitions = async (params?: {
|
||||
name?: string
|
||||
category?: string
|
||||
status?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await tagDefinitionApi.getTagDefinitionList(params)
|
||||
definitions.value = response.data.definitions
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取标签定义列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签定义详情
|
||||
const fetchDefinitionDetail = async (tagId: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await tagDefinitionApi.getTagDefinitionDetail(tagId)
|
||||
currentDefinition.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取标签定义详情失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建标签定义
|
||||
const createDefinition = async (data: Partial<TagDefinition>) => {
|
||||
try {
|
||||
const response = await tagDefinitionApi.createTagDefinition(data)
|
||||
await fetchDefinitions()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('创建标签定义失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标签定义
|
||||
const updateDefinition = async (tagId: string, data: Partial<TagDefinition>) => {
|
||||
try {
|
||||
const response = await tagDefinitionApi.updateTagDefinition(tagId, data)
|
||||
await fetchDefinitions()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新标签定义失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除标签定义
|
||||
const deleteDefinition = async (tagId: string) => {
|
||||
try {
|
||||
await tagDefinitionApi.deleteTagDefinition(tagId)
|
||||
await fetchDefinitions()
|
||||
} catch (error) {
|
||||
console.error('删除标签定义失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取启用的标签定义列表(用于下拉选择)
|
||||
const getActiveDefinitions = async () => {
|
||||
return await fetchDefinitions({ status: 0 })
|
||||
}
|
||||
|
||||
// 重置当前标签定义
|
||||
const resetCurrentDefinition = () => {
|
||||
currentDefinition.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
definitions,
|
||||
currentDefinition,
|
||||
loading,
|
||||
fetchDefinitions,
|
||||
fetchDefinitionDetail,
|
||||
createDefinition,
|
||||
updateDefinition,
|
||||
deleteDefinition,
|
||||
getActiveDefinitions,
|
||||
resetCurrentDefinition
|
||||
}
|
||||
})
|
||||
|
||||
154
Moncter/TaskShow/src/store/tagTask.ts
Normal file
154
Moncter/TaskShow/src/store/tagTask.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { TagTask, TaskExecution } from '@/types'
|
||||
import * as tagTaskApi from '@/api/tagTask'
|
||||
|
||||
export const useTagTaskStore = defineStore('tagTask', () => {
|
||||
const tasks = ref<TagTask[]>([])
|
||||
const currentTask = ref<TagTask | null>(null)
|
||||
const executions = ref<TaskExecution[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取任务列表
|
||||
const fetchTasks = async (params?: {
|
||||
name?: string
|
||||
task_type?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await tagTaskApi.getTagTaskList(params || {})
|
||||
tasks.value = response.data.tasks
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务详情
|
||||
const fetchTaskDetail = async (taskId: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await tagTaskApi.getTagTaskDetail(taskId)
|
||||
currentTask.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
const createTask = async (data: Partial<TagTask>) => {
|
||||
try {
|
||||
const response = await tagTaskApi.createTagTask(data)
|
||||
await fetchTasks()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('创建任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
const updateTask = async (taskId: string, data: Partial<TagTask>) => {
|
||||
try {
|
||||
const response = await tagTaskApi.updateTagTask(taskId, data)
|
||||
await fetchTasks()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deleteTask = async (taskId: string) => {
|
||||
try {
|
||||
await tagTaskApi.deleteTagTask(taskId)
|
||||
await fetchTasks()
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 启动任务
|
||||
const startTask = async (taskId: string) => {
|
||||
try {
|
||||
await tagTaskApi.startTagTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('启动任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
const pauseTask = async (taskId: string) => {
|
||||
try {
|
||||
await tagTaskApi.pauseTagTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('暂停任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 停止任务
|
||||
const stopTask = async (taskId: string) => {
|
||||
try {
|
||||
await tagTaskApi.stopTagTask(taskId)
|
||||
await fetchTaskDetail(taskId)
|
||||
} catch (error) {
|
||||
console.error('停止任务失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取执行记录
|
||||
const fetchExecutions = async (taskId: string, params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => {
|
||||
try {
|
||||
const response = await tagTaskApi.getTagTaskExecutions(taskId, params)
|
||||
executions.value = response.data.executions
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取执行记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置当前任务
|
||||
const resetCurrentTask = () => {
|
||||
currentTask.value = null
|
||||
executions.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
currentTask,
|
||||
executions,
|
||||
loading,
|
||||
fetchTasks,
|
||||
fetchTaskDetail,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
startTask,
|
||||
pauseTask,
|
||||
stopTask,
|
||||
fetchExecutions,
|
||||
resetCurrentTask
|
||||
}
|
||||
})
|
||||
|
||||
35
Moncter/TaskShow/src/store/user.ts
Normal file
35
Moncter/TaskShow/src/store/user.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 用户 token
|
||||
const token = ref<string | null>(null)
|
||||
|
||||
// 初始化 token(从 localStorage 读取)
|
||||
const initToken = () => {
|
||||
const storedToken = localStorage.getItem('token')
|
||||
if (storedToken) {
|
||||
token.value = storedToken
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 token
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 清除用户信息
|
||||
const clearUser = () => {
|
||||
token.value = null
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
initToken,
|
||||
setToken,
|
||||
clearUser
|
||||
}
|
||||
})
|
||||
|
||||
33
Moncter/TaskShow/src/types/api.ts
Normal file
33
Moncter/TaskShow/src/types/api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// API 响应类型定义
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 请求配置类型
|
||||
export interface RequestConfig {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
data?: any
|
||||
params?: any
|
||||
headers?: Record<string, string>
|
||||
timeout?: number
|
||||
showLoading?: boolean
|
||||
showError?: boolean
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
export interface PageParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
export interface PageResponse<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
299
Moncter/TaskShow/src/types/index.ts
Normal file
299
Moncter/TaskShow/src/types/index.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* 通用类型定义
|
||||
*/
|
||||
|
||||
// API 响应结构
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PageResponse<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
// 数据采集任务
|
||||
export interface LookupConfig {
|
||||
from: string
|
||||
local_field: string
|
||||
foreign_field: string
|
||||
as: string
|
||||
unwrap?: boolean
|
||||
preserve_null?: boolean
|
||||
}
|
||||
|
||||
export interface DataCollectionTask {
|
||||
task_id: string
|
||||
name: string
|
||||
description?: string
|
||||
data_source_id: string // 源数据源ID
|
||||
database: string // 源数据库
|
||||
collection?: string // 源集合(单集合模式)
|
||||
collections?: string[] // 源集合列表(多集合模式)
|
||||
multi_collection?: boolean
|
||||
target_data_source_id?: string // 目标数据源ID
|
||||
target_database?: string // 目标数据库
|
||||
target_collection?: string // 目标集合
|
||||
target_type?: string // 目标类型:consumption_record=消费记录,generic=通用集合
|
||||
mode: 'batch' | 'realtime'
|
||||
field_mappings: FieldMapping[]
|
||||
collection_field_mappings?: Record<string, FieldMapping[]>
|
||||
lookups?: LookupConfig[]
|
||||
collection_lookups?: Record<string, LookupConfig[]>
|
||||
filter_conditions?: FilterCondition[]
|
||||
schedule: ScheduleConfig
|
||||
status: 'pending' | 'running' | 'paused' | 'stopped' | 'error'
|
||||
progress: TaskProgress
|
||||
statistics?: TaskStatistics
|
||||
created_by?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 字段映射
|
||||
export interface FieldMapping {
|
||||
source_field: string
|
||||
target_field: string
|
||||
transform?: string
|
||||
value_mapping?: Array<{
|
||||
source_value: string
|
||||
target_value: number
|
||||
}>
|
||||
}
|
||||
|
||||
// 过滤条件
|
||||
export interface FilterCondition {
|
||||
field: string
|
||||
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin'
|
||||
value: any
|
||||
}
|
||||
|
||||
// 调度配置
|
||||
export interface ScheduleConfig {
|
||||
enabled: boolean
|
||||
cron?: string
|
||||
}
|
||||
|
||||
// 任务进度
|
||||
export interface TaskProgress {
|
||||
status: 'idle' | 'running' | 'paused' | 'completed' | 'error'
|
||||
processed_count?: number
|
||||
success_count?: number
|
||||
error_count?: number
|
||||
total_count?: number
|
||||
percentage?: number
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
last_sync_time?: string
|
||||
}
|
||||
|
||||
// 任务统计
|
||||
export interface TaskStatistics {
|
||||
total_processed?: number
|
||||
total_success?: number
|
||||
total_error?: number
|
||||
last_run_time?: string
|
||||
}
|
||||
|
||||
// 数据源
|
||||
export interface DataSource {
|
||||
id: string
|
||||
name?: string
|
||||
type: string
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
}
|
||||
|
||||
// 标签任务
|
||||
export interface TagTask {
|
||||
task_id: string
|
||||
name: string
|
||||
description?: string
|
||||
task_type: 'full' | 'incremental' | 'specific'
|
||||
target_tag_ids: string[]
|
||||
user_scope: UserScope
|
||||
schedule: ScheduleConfig
|
||||
config: TagTaskConfig
|
||||
status: 'pending' | 'running' | 'paused' | 'stopped' | 'completed' | 'error'
|
||||
progress: TagTaskProgress
|
||||
statistics?: TagTaskStatistics
|
||||
created_by?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 用户范围
|
||||
export interface UserScope {
|
||||
type: 'all' | 'list' | 'filter'
|
||||
user_ids?: string[]
|
||||
filter_conditions?: FilterCondition[]
|
||||
}
|
||||
|
||||
// 标签任务配置
|
||||
export interface TagTaskConfig {
|
||||
concurrency: number
|
||||
batch_size: number
|
||||
error_handling: 'skip' | 'stop' | 'retry'
|
||||
}
|
||||
|
||||
// 标签任务进度
|
||||
export interface TagTaskProgress {
|
||||
total_users: number
|
||||
processed_users: number
|
||||
success_count: number
|
||||
error_count: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
// 标签任务统计
|
||||
export interface TagTaskStatistics {
|
||||
total_executions: number
|
||||
success_executions: number
|
||||
failed_executions: number
|
||||
last_run_time?: string
|
||||
}
|
||||
|
||||
// 任务执行记录
|
||||
export interface TaskExecution {
|
||||
execution_id: string
|
||||
task_id: string
|
||||
started_at: string
|
||||
finished_at?: string
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled'
|
||||
processed_users?: number
|
||||
success_count?: number
|
||||
error_count?: number
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
// 标签定义
|
||||
export interface TagDefinition {
|
||||
tag_id: string
|
||||
tag_code: string
|
||||
tag_name: string
|
||||
category: string
|
||||
description?: string
|
||||
rule_type: 'simple' | 'pipeline' | 'custom'
|
||||
rule_config: RuleConfig
|
||||
update_frequency: 'real_time' | 'daily' | 'weekly' | 'monthly'
|
||||
status: number // 0-启用,1-禁用
|
||||
priority?: number
|
||||
version?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 规则配置
|
||||
export interface RuleConfig {
|
||||
rule_type: 'simple' | 'pipeline' | 'custom'
|
||||
conditions: RuleCondition[]
|
||||
tag_value?: any
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
// 规则条件
|
||||
export interface RuleCondition {
|
||||
field: string
|
||||
operator: '>' | '>=' | '<' | '<=' | '=' | '!=' | 'in' | 'not_in'
|
||||
value: any
|
||||
}
|
||||
|
||||
// 标签条件(用于筛选)
|
||||
export interface TagCondition {
|
||||
tag_code: string
|
||||
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'not_contains'
|
||||
value: any
|
||||
}
|
||||
|
||||
// 用户标签
|
||||
export interface UserTag {
|
||||
tag_id: string
|
||||
tag_code?: string
|
||||
tag_name?: string
|
||||
category?: string
|
||||
tag_value: string
|
||||
tag_value_type: string
|
||||
confidence: number
|
||||
effective_time?: string
|
||||
expire_time?: string
|
||||
update_time: string
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
export interface UserInfo {
|
||||
user_id: string
|
||||
name?: string
|
||||
phone?: string
|
||||
total_amount?: number
|
||||
total_count?: number
|
||||
last_consume_time?: string
|
||||
}
|
||||
|
||||
// 标签统计信息
|
||||
export interface TagStatistics {
|
||||
value_distribution: Array<{
|
||||
value: string
|
||||
count: number
|
||||
}>
|
||||
trend_data: Array<{
|
||||
date: string
|
||||
count: number
|
||||
}>
|
||||
coverage_stats: Array<{
|
||||
tag_id: string
|
||||
tag_name: string
|
||||
total_users: number
|
||||
tagged_users: number
|
||||
coverage_rate: number
|
||||
}>
|
||||
}
|
||||
|
||||
// 标签历史记录
|
||||
export interface TagHistoryItem {
|
||||
user_id: string
|
||||
tag_id: string
|
||||
tag_name: string | null
|
||||
old_value?: string | null
|
||||
new_value: string
|
||||
change_reason?: string | null
|
||||
change_time: string | null
|
||||
operator?: string | null
|
||||
}
|
||||
|
||||
// 标签历史响应
|
||||
export interface TagHistoryResponse {
|
||||
items: TagHistoryItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 人群快照
|
||||
export interface TagCohort {
|
||||
cohort_id: string
|
||||
name: string
|
||||
description?: string
|
||||
conditions: TagCondition[]
|
||||
logic: 'AND' | 'OR'
|
||||
user_ids: string[]
|
||||
user_count: number
|
||||
created_by?: string
|
||||
created_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 人群快照列表项
|
||||
export interface TagCohortListItem {
|
||||
cohort_id: string
|
||||
name: string
|
||||
user_count: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
106
Moncter/TaskShow/src/utils/format.ts
Normal file
106
Moncter/TaskShow/src/utils/format.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 日期时间格式化工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param date 日期对象或时间戳或日期字符串
|
||||
* @param format 格式字符串,默认 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export const formatDateTime = (
|
||||
date: Date | number | string | null | undefined,
|
||||
format: string = 'YYYY-MM-DD HH:mm:ss'
|
||||
): string => {
|
||||
if (!date) return '-'
|
||||
|
||||
try {
|
||||
let d: Date
|
||||
|
||||
if (date instanceof Date) {
|
||||
d = date
|
||||
} else if (typeof date === 'string') {
|
||||
// 处理 ISO 8601 格式字符串
|
||||
d = new Date(date)
|
||||
} else if (typeof date === 'number') {
|
||||
d = new Date(date)
|
||||
} else {
|
||||
// 如果是对象,尝试转换为字符串再解析
|
||||
d = new Date(String(date))
|
||||
}
|
||||
|
||||
// 检查日期是否有效
|
||||
if (!(d instanceof Date) || isNaN(d.getTime())) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
} catch (error) {
|
||||
console.error('formatDateTime error:', error, date)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param date 日期对象或时间戳或日期字符串
|
||||
* @returns 格式化后的日期字符串 YYYY-MM-DD
|
||||
*/
|
||||
export const formatDate = (date: Date | number | string | null | undefined): string => {
|
||||
return formatDateTime(date, 'YYYY-MM-DD')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param date 日期对象或时间戳或日期字符串
|
||||
* @returns 格式化后的时间字符串 HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (date: Date | number | string | null | undefined): string => {
|
||||
return formatDateTime(date, 'HH:mm:ss')
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对时间格式化(如:1分钟前、2小时前)
|
||||
* @param date 日期对象或时间戳或日期字符串
|
||||
* @returns 相对时间字符串
|
||||
*/
|
||||
export const formatRelativeTime = (date: Date | number | string | null | undefined): string => {
|
||||
if (!date) return '-'
|
||||
|
||||
const d = typeof date === 'string' || typeof date === 'number'
|
||||
? new Date(date)
|
||||
: date
|
||||
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - d.getTime()
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天前`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`
|
||||
} else {
|
||||
return '刚刚'
|
||||
}
|
||||
}
|
||||
|
||||
8
Moncter/TaskShow/src/utils/index.ts
Normal file
8
Moncter/TaskShow/src/utils/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './format'
|
||||
export * from './mask'
|
||||
export * from './validator'
|
||||
|
||||
71
Moncter/TaskShow/src/utils/mask.ts
Normal file
71
Moncter/TaskShow/src/utils/mask.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 数据脱敏工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 脱敏手机号
|
||||
* @param phone 手机号
|
||||
* @returns 脱敏后的手机号,如:138****8000
|
||||
*/
|
||||
export const maskPhone = (phone: string | null | undefined): string => {
|
||||
if (!phone) return '-'
|
||||
if (phone.length !== 11) return phone
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏身份证号
|
||||
* @param idCard 身份证号
|
||||
* @returns 脱敏后的身份证号,如:110101********1234
|
||||
*/
|
||||
export const maskIdCard = (idCard: string | null | undefined): string => {
|
||||
if (!idCard) return '-'
|
||||
if (idCard.length === 18) {
|
||||
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
|
||||
} else if (idCard.length === 15) {
|
||||
return idCard.replace(/(\d{6})\d{6}(\d{3})/, '$1******$2')
|
||||
}
|
||||
return idCard
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏银行卡号
|
||||
* @param cardNo 银行卡号
|
||||
* @returns 脱敏后的银行卡号,如:6222 **** **** 1234
|
||||
*/
|
||||
export const maskBankCard = (cardNo: string | null | undefined): string => {
|
||||
if (!cardNo) return '-'
|
||||
if (cardNo.length < 8) return cardNo
|
||||
const start = cardNo.substring(0, 4)
|
||||
const end = cardNo.substring(cardNo.length - 4)
|
||||
const middle = '*'.repeat(Math.max(0, cardNo.length - 8))
|
||||
return `${start} ${middle} ${end}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏姓名
|
||||
* @param name 姓名
|
||||
* @returns 脱敏后的姓名,如:张*、李**
|
||||
*/
|
||||
export const maskName = (name: string | null | undefined): string => {
|
||||
if (!name) return '-'
|
||||
if (name.length === 1) return name
|
||||
if (name.length === 2) return `${name[0]}*`
|
||||
return `${name[0]}${'*'.repeat(name.length - 2)}${name[name.length - 1]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏邮箱
|
||||
* @param email 邮箱
|
||||
* @returns 脱敏后的邮箱,如:abc****@example.com
|
||||
*/
|
||||
export const maskEmail = (email: string | null | undefined): string => {
|
||||
if (!email) return '-'
|
||||
const [username, domain] = email.split('@')
|
||||
if (!domain) return email
|
||||
if (username.length <= 2) {
|
||||
return `${username[0]}*@${domain}`
|
||||
}
|
||||
return `${username.substring(0, 2)}${'*'.repeat(Math.max(0, username.length - 2))}@${domain}`
|
||||
}
|
||||
|
||||
191
Moncter/TaskShow/src/utils/request.ts
Normal file
191
Moncter/TaskShow/src/utils/request.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { useUserStore } from '@/store'
|
||||
import type { ApiResponse, RequestConfig } from '@/types/api'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 添加 token
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
}
|
||||
|
||||
// 显示 loading
|
||||
if (config.showLoading !== false) {
|
||||
config.loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '加载中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
const config = response.config as any
|
||||
|
||||
// 关闭 loading
|
||||
if (config.loadingInstance) {
|
||||
config.loadingInstance.close()
|
||||
}
|
||||
|
||||
const res = response.data
|
||||
|
||||
// 根据业务状态码处理
|
||||
if (res.code === 200 || res.code === 0) {
|
||||
return res
|
||||
} else {
|
||||
// 业务错误
|
||||
if (config.showError !== false) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
}
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
}
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
const config = error.config as any
|
||||
|
||||
// 关闭 loading
|
||||
if (config?.loadingInstance) {
|
||||
config.loadingInstance.close()
|
||||
}
|
||||
|
||||
// HTTP 错误处理
|
||||
if (error.response) {
|
||||
const status = error.response.status
|
||||
const userStore = useUserStore()
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
ElMessage.error('未授权,请重新登录')
|
||||
userStore.clearUser()
|
||||
// 可以在这里跳转到登录页
|
||||
// router.push('/login')
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('拒绝访问')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求错误,未找到该资源')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器错误')
|
||||
break
|
||||
case 502:
|
||||
ElMessage.error('网关错误')
|
||||
break
|
||||
case 503:
|
||||
ElMessage.error('服务不可用')
|
||||
break
|
||||
case 504:
|
||||
ElMessage.error('网关超时')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(`请求失败: ${error.response.statusText}`)
|
||||
}
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
} else {
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 封装请求方法
|
||||
class Request {
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
get<T = any>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return service.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return service.request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return service.request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
delete<T = any>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return service.request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
*/
|
||||
patch<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return service.request({
|
||||
url,
|
||||
method: 'PATCH',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||||
return service.request(config)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出请求实例
|
||||
export const request = new Request()
|
||||
|
||||
// 导出 axios 实例(用于特殊需求)
|
||||
export default service
|
||||
|
||||
78
Moncter/TaskShow/src/utils/validator.ts
Normal file
78
Moncter/TaskShow/src/utils/validator.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 表单验证工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param phone 手机号
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validatePhone = (phone: string): boolean => {
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
return phoneReg.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param email 邮箱
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validateEmail = (email: string): boolean => {
|
||||
const emailReg = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return emailReg.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号
|
||||
* @param idCard 身份证号
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validateIdCard = (idCard: string): boolean => {
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return idCardReg.test(idCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
* @param url URL地址
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validateUrl = (url: string): boolean => {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Cron表达式
|
||||
* @param cron Cron表达式
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validateCron = (cron: string): boolean => {
|
||||
const cronReg = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/
|
||||
return cronReg.test(cron)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否为正整数
|
||||
* @param value 值
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validatePositiveInteger = (value: number | string): boolean => {
|
||||
const num = typeof value === 'string' ? Number(value) : value
|
||||
return Number.isInteger(num) && num > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否为正数
|
||||
* @param value 值
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export const validatePositiveNumber = (value: number | string): boolean => {
|
||||
const num = typeof value === 'string' ? Number(value) : value
|
||||
return !isNaN(num) && num > 0
|
||||
}
|
||||
|
||||
270
Moncter/TaskShow/src/views/Dashboard/index.vue
Normal file
270
Moncter/TaskShow/src/views/Dashboard/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #409eff;">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.dataCollectionTasks }}</div>
|
||||
<div class="stat-label">数据采集任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #67c23a;">
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.tagTasks }}</div>
|
||||
<div class="stat-label">标签任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #e6a23c;">
|
||||
<el-icon><Filter /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.runningTasks }}</div>
|
||||
<div class="stat-label">运行中任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #f56c6c;">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totalUsers }}</div>
|
||||
<div class="stat-label">用户总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最近任务</span>
|
||||
<el-button type="primary" @click="goToTaskList">查看更多</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="recentTasks" style="width: 100%">
|
||||
<el-table-column prop="name" label="任务名称" />
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.type === 'data_collection'" type="primary">数据采集</el-tag>
|
||||
<el-tag v-else type="success">标签任务</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<StatusBadge :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>快速操作</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="quick-actions">
|
||||
<el-button type="primary" @click="goToCreateDataCollectionTask">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建数据采集任务
|
||||
</el-button>
|
||||
<el-button type="success" @click="goToCreateTagTask">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建标签任务
|
||||
</el-button>
|
||||
<el-button type="warning" @click="goToTagFilter">
|
||||
<el-icon><Filter /></el-icon>
|
||||
标签筛选
|
||||
</el-button>
|
||||
<el-button type="info" @click="goToTagQuery">
|
||||
<el-icon><Search /></el-icon>
|
||||
标签查询
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Document, PriceTag, Filter, User, Plus, Search } from '@element-plus/icons-vue'
|
||||
import StatusBadge from '@/components/StatusBadge/index.vue'
|
||||
import { useDataCollectionStore } from '@/store'
|
||||
import { useTagTaskStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const dataCollectionStore = useDataCollectionStore()
|
||||
const tagTaskStore = useTagTaskStore()
|
||||
|
||||
const stats = ref({
|
||||
dataCollectionTasks: 0,
|
||||
tagTasks: 0,
|
||||
runningTasks: 0,
|
||||
totalUsers: 0
|
||||
})
|
||||
|
||||
const recentTasks = ref<Array<{
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
updated_at: string
|
||||
}>>([])
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 加载数据采集任务列表
|
||||
const collectionResult = await dataCollectionStore.fetchTasks({ page: 1, page_size: 1 })
|
||||
stats.value.dataCollectionTasks = collectionResult.total
|
||||
|
||||
// 统计运行中的任务
|
||||
const runningCollectionTasks = dataCollectionStore.tasks.filter(
|
||||
t => t.status === 'running'
|
||||
).length
|
||||
|
||||
// 加载标签任务列表
|
||||
const tagResult = await tagTaskStore.fetchTasks({ page: 1, page_size: 1 })
|
||||
stats.value.tagTasks = tagResult.total
|
||||
|
||||
// 统计运行中的标签任务
|
||||
const runningTagTasks = tagTaskStore.tasks.filter(
|
||||
t => t.status === 'running'
|
||||
).length
|
||||
|
||||
stats.value.runningTasks = runningCollectionTasks + runningTagTasks
|
||||
|
||||
// 合并最近任务
|
||||
const allTasks = [
|
||||
...dataCollectionStore.tasks.slice(0, 5).map(t => ({
|
||||
name: t.name,
|
||||
type: 'data_collection',
|
||||
status: t.status,
|
||||
updated_at: formatDateTime(t.updated_at)
|
||||
})),
|
||||
...tagTaskStore.tasks.slice(0, 5).map(t => ({
|
||||
name: t.name,
|
||||
type: 'tag_task',
|
||||
status: t.status,
|
||||
updated_at: formatDateTime(t.updated_at)
|
||||
}))
|
||||
]
|
||||
recentTasks.value = allTasks
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||
.slice(0, 5)
|
||||
|
||||
// TODO: 加载用户总数(需要后端提供接口)
|
||||
// stats.value.totalUsers = await getUserTotal()
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const goToTaskList = () => {
|
||||
router.push('/data-collection/tasks')
|
||||
}
|
||||
|
||||
const goToCreateDataCollectionTask = () => {
|
||||
router.push('/data-collection/tasks/create')
|
||||
}
|
||||
|
||||
const goToCreateTagTask = () => {
|
||||
router.push('/tag-tasks/create')
|
||||
}
|
||||
|
||||
const goToTagFilter = () => {
|
||||
router.push('/tag-filter')
|
||||
}
|
||||
|
||||
const goToTagQuery = () => {
|
||||
router.push('/tag-query/user')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard {
|
||||
.stat-card {
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.el-button {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
205
Moncter/TaskShow/src/views/DataCollection/TaskDetail.vue
Normal file
205
Moncter/TaskShow/src/views/DataCollection/TaskDetail.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="task-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>任务详情</span>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="task?.status === 'pending' || task?.status === 'paused'"
|
||||
type="success"
|
||||
@click="handleStart"
|
||||
>
|
||||
启动
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="task?.status === 'running'"
|
||||
type="warning"
|
||||
@click="handlePause"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="task?.status === 'running' || task?.status === 'paused'"
|
||||
type="danger"
|
||||
@click="handleStop"
|
||||
>
|
||||
停止
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions title="基本信息" :column="2" border>
|
||||
<el-descriptions-item label="任务名称">{{ task?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务状态">
|
||||
<StatusBadge :status="task?.status || 'pending'" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据源">{{ task?.data_source_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库">{{ task?.database }}</el-descriptions-item>
|
||||
<el-descriptions-item label="集合">{{ task?.collection }}</el-descriptions-item>
|
||||
<el-descriptions-item label="采集模式">
|
||||
<el-tag v-if="task?.mode === 'realtime'" type="success">实时</el-tag>
|
||||
<el-tag v-else type="info">批量</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(task?.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(task?.updated_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务描述" :span="2">{{ task?.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div class="progress-section" v-if="task?.progress">
|
||||
<h3>进度信息</h3>
|
||||
<ProgressDisplay
|
||||
:title="'任务进度'"
|
||||
:percentage="task.progress.percentage || 0"
|
||||
:total="task.progress.total_count"
|
||||
:processed="task.progress.processed_count"
|
||||
:success="task.progress.success_count"
|
||||
:error="task.progress.error_count"
|
||||
:start-time="task.progress.start_time"
|
||||
:end-time="task.progress.end_time"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 字段映射 -->
|
||||
<div class="field-mappings" v-if="task?.field_mappings?.length">
|
||||
<h3>字段映射</h3>
|
||||
<el-table :data="task.field_mappings" border>
|
||||
<el-table-column prop="source_field" label="源字段" />
|
||||
<el-table-column prop="target_field" label="目标字段" />
|
||||
<el-table-column prop="transform" label="转换函数" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 过滤条件 -->
|
||||
<div class="filter-conditions" v-if="task?.filter_conditions?.length">
|
||||
<h3>过滤条件</h3>
|
||||
<el-table :data="task.filter_conditions" border>
|
||||
<el-table-column prop="field" label="字段" />
|
||||
<el-table-column prop="operator" label="运算符" />
|
||||
<el-table-column prop="value" label="值" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import StatusBadge from '@/components/StatusBadge/index.vue'
|
||||
import ProgressDisplay from '@/components/ProgressDisplay/index.vue'
|
||||
import { useDataCollectionStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useDataCollectionStore()
|
||||
|
||||
const loading = computed(() => store.loading)
|
||||
const task = computed(() => store.currentTask)
|
||||
let progressTimer: number | null = null
|
||||
|
||||
const loadTaskDetail = async () => {
|
||||
try {
|
||||
await store.fetchTaskDetail(route.params.id as string)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadProgress = async () => {
|
||||
if (!task.value) return
|
||||
|
||||
try {
|
||||
await store.fetchTaskProgress(route.params.id as string)
|
||||
} catch (error) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
await store.startTask(route.params.id as string)
|
||||
ElMessage.success('任务已启动')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '启动任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
await store.pauseTask(route.params.id as string)
|
||||
ElMessage.success('任务已暂停')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '暂停任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要停止该任务吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.stopTask(route.params.id as string)
|
||||
ElMessage.success('任务已停止')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '停止任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/data-collection/tasks/${route.params.id}/edit`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTaskDetail()
|
||||
|
||||
// 如果任务正在运行,定时刷新进度
|
||||
progressTimer = window.setInterval(() => {
|
||||
if (task.value?.status === 'running') {
|
||||
loadProgress()
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer)
|
||||
}
|
||||
store.resetCurrentTask()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-section,
|
||||
.field-mappings,
|
||||
.filter-conditions {
|
||||
margin-top: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1676
Moncter/TaskShow/src/views/DataCollection/TaskForm.vue
Normal file
1676
Moncter/TaskShow/src/views/DataCollection/TaskForm.vue
Normal file
File diff suppressed because it is too large
Load Diff
382
Moncter/TaskShow/src/views/DataCollection/TaskList.vue
Normal file
382
Moncter/TaskShow/src/views/DataCollection/TaskList.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据采集任务列表</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建任务
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="任务名称">
|
||||
<el-input
|
||||
v-model="filters.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="待启动" value="pending" />
|
||||
<el-option label="运行中" value="running" />
|
||||
<el-option label="已暂停" value="paused" />
|
||||
<el-option label="已停止" value="stopped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="错误" value="error" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="filter-buttons">
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table :data="taskList" v-loading="loading" >
|
||||
<el-table-column prop="name" label="任务名称" />
|
||||
<el-table-column prop="data_source_id" label="数据源" width="200">
|
||||
<template #default="{ row }">
|
||||
{{ getDataSourceName(row.data_source_id) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="database" label="数据库" width="200" />
|
||||
<el-table-column prop="collection" label="集合" width="200" />
|
||||
<el-table-column prop="mode" label="模式" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.mode === 'realtime'" type="success">实时</el-tag>
|
||||
<el-tag v-else type="info">批量</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<StatusBadge :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress.percentage" label="进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.progress?.percentage || 0"
|
||||
:status="row.progress?.status === 'error' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content:space-between;">
|
||||
<!-- 状态控制按钮(根据任务状态显示) -->
|
||||
<!-- 启动按钮:pending(待启动)、paused(已暂停)、stopped(已停止)、completed(已完成)、error(错误) -->
|
||||
<el-button
|
||||
v-if="['pending', 'paused', 'stopped', 'completed', 'error'].includes(row.status)"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleStart(row)"
|
||||
>
|
||||
{{ ['completed', 'error', 'stopped'].includes(row.status) ? '重新启动' : row.status === 'paused' ? '恢复' : '启动' }}
|
||||
</el-button>
|
||||
<!-- 暂停按钮:仅在 running(运行中) 状态显示 -->
|
||||
<el-button
|
||||
v-if="row.status === 'running'"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handlePause(row)"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<!-- 停止按钮:running(运行中) 或 paused(已暂停) 状态显示 -->
|
||||
<el-button
|
||||
v-if="row.status === 'running' || row.status === 'paused'"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleStop(row)"
|
||||
>
|
||||
停止
|
||||
</el-button>
|
||||
|
||||
<!-- 其他操作(下拉菜单) -->
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleDropdownCommand(cmd, row)">
|
||||
<el-button size="small" type="primary" plain>
|
||||
更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<!-- 查看:所有状态可用 -->
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-dropdown-item>
|
||||
<!-- 编辑:只有非运行中状态可用(pending, paused, stopped, completed, error) -->
|
||||
<el-dropdown-item
|
||||
command="edit"
|
||||
:disabled="row.status === 'running'"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-dropdown-item>
|
||||
<!-- 复制:所有状态可用 -->
|
||||
<el-dropdown-item command="duplicate">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
复制
|
||||
</el-dropdown-item>
|
||||
<!-- 删除:所有状态可用,但运行中/已暂停时会先停止任务 -->
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span style="color: #f56c6c;">删除</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, ArrowDown, View, Edit, CopyDocument, Delete } from '@element-plus/icons-vue'
|
||||
import StatusBadge from '@/components/StatusBadge/index.vue'
|
||||
import { useDataCollectionStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import type { DataCollectionTask } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useDataCollectionStore()
|
||||
|
||||
const filters = ref({
|
||||
name: '',
|
||||
status: ''
|
||||
})
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const taskList = computed(() => store.tasks)
|
||||
const loading = computed(() => store.loading)
|
||||
const dataSources = computed(() => store.dataSources)
|
||||
|
||||
// 根据数据源ID获取数据源名称
|
||||
const getDataSourceName = (dataSourceId: string): string => {
|
||||
if (!dataSourceId) return '-'
|
||||
const dataSource = dataSources.value.find(ds => ds.id === dataSourceId)
|
||||
return dataSource?.name || dataSourceId
|
||||
}
|
||||
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const result = await store.fetchTasks({
|
||||
...filters.value,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
pagination.value.total = result.total
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载任务列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.page = 1
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
name: '',
|
||||
status: ''
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push('/data-collection/tasks/create')
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
router.push(`/data-collection/tasks/${row.task_id}`)
|
||||
}
|
||||
|
||||
const handleEdit = (row: DataCollectionTask) => {
|
||||
// 运行中的任务不允许编辑
|
||||
if (row.status === 'running') {
|
||||
ElMessage.warning('运行中的任务不允许编辑,请先停止任务')
|
||||
return
|
||||
}
|
||||
router.push(`/data-collection/tasks/${row.task_id}/edit`)
|
||||
}
|
||||
|
||||
const handleDuplicate = async (row: DataCollectionTask) => {
|
||||
try {
|
||||
await store.duplicateTask(row.task_id)
|
||||
ElMessage.success('任务复制成功')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '复制任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStart = async (row: DataCollectionTask) => {
|
||||
try {
|
||||
await store.startTask(row.task_id)
|
||||
ElMessage.success('任务已启动')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '启动任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = async (row: DataCollectionTask) => {
|
||||
try {
|
||||
await store.pauseTask(row.task_id)
|
||||
ElMessage.success('任务已暂停')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '暂停任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async (row: DataCollectionTask) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要停止该任务吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.stopTask(row.task_id)
|
||||
ElMessage.success('任务已停止')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '停止任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: DataCollectionTask) => {
|
||||
try {
|
||||
// 运行中或已暂停的任务需要特别提示(后端会自动先停止)
|
||||
const isRunningOrPaused = row.status === 'running' || row.status === 'paused'
|
||||
const confirmMessage = isRunningOrPaused
|
||||
? `任务当前状态为"${row.status === 'running' ? '运行中' : '已暂停'}",删除任务将先停止该任务。确定要删除吗?`
|
||||
: '确定要删除该任务吗?'
|
||||
|
||||
await ElMessageBox.confirm(confirmMessage, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
// 传递当前筛选和分页参数,保持列表状态
|
||||
// store.deleteTask 内部会自动刷新列表,不需要再次调用 loadTasks()
|
||||
const result = await store.deleteTask(row.task_id, {
|
||||
...filters.value,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
pagination.value.total = result.total
|
||||
ElMessage.success('任务已删除')
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
// 处理下拉菜单命令
|
||||
const handleDropdownCommand = (command: string, row: DataCollectionTask) => {
|
||||
switch (command) {
|
||||
case 'view':
|
||||
handleView(row)
|
||||
break
|
||||
case 'edit':
|
||||
// 编辑按钮可能被禁用,但点击时仍会触发,这里再次检查
|
||||
if (row.status === 'running') {
|
||||
ElMessage.warning('运行中的任务不允许编辑,请先停止任务')
|
||||
return
|
||||
}
|
||||
handleEdit(row)
|
||||
break
|
||||
case 'duplicate':
|
||||
handleDuplicate(row)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载数据源列表,用于显示数据源名称
|
||||
await store.fetchDataSources()
|
||||
loadTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-list {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
.filter-buttons{
|
||||
.el-button{margin-left: 20px;}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.el-button+.el-button {
|
||||
margin-left: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
268
Moncter/TaskShow/src/views/DataSource/Form.vue
Normal file
268
Moncter/TaskShow/src/views/DataSource/Form.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="data-source-form">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ isEdit ? '编辑数据源' : '创建数据源' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider>基本信息</el-divider>
|
||||
<el-form-item label="数据源名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入数据源名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据源类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择数据源类型" @change="handleTypeChange">
|
||||
<el-option label="MongoDB" value="mongodb" />
|
||||
<el-option label="MySQL" value="mysql" />
|
||||
<el-option label="PostgreSQL" value="postgresql" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 连接配置 -->
|
||||
<el-divider>连接配置</el-divider>
|
||||
<el-form-item label="主机地址" prop="host">
|
||||
<el-input v-model="form.host" placeholder="例如: 192.168.1.106" />
|
||||
</el-form-item>
|
||||
<el-form-item label="端口" prop="port">
|
||||
<el-input-number v-model="form.port" :min="1" :max="65535" placeholder="例如: 27017" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据库名" prop="database">
|
||||
<el-input v-model="form.database" placeholder="请输入数据库名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
:placeholder="isEdit ? '留空则不修改密码' : '请输入密码'"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.type === 'mongodb'"
|
||||
label="认证数据库"
|
||||
prop="auth_source"
|
||||
>
|
||||
<el-input v-model="form.auth_source" placeholder="例如: admin" />
|
||||
<div class="form-tip">MongoDB认证数据库,默认为admin</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 标签引擎数据库标识 -->
|
||||
<el-form-item label="标签引擎数据库" prop="is_tag_engine">
|
||||
<el-switch
|
||||
v-model="form.is_tag_engine"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
标识此数据源是否为标签引擎数据库(ckb数据库)。设置为"是"时,系统会自动将其他数据源设置为"否",确保只有一个标签引擎数据库。
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleTestConnection" :loading="testing">
|
||||
测试连接
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import { useDataSourceStore } from '@/store/dataSource'
|
||||
import type { DataSource } from '@/api/dataSource'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const store = useDataSourceStore()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const testing = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive<Partial<DataSource>>({
|
||||
name: '',
|
||||
type: 'mongodb',
|
||||
host: '',
|
||||
port: 27017,
|
||||
database: '',
|
||||
username: '',
|
||||
password: '',
|
||||
auth_source: 'admin',
|
||||
description: '',
|
||||
is_tag_engine: false,
|
||||
status: 1,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入数据源名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择数据源类型', trigger: 'change' }],
|
||||
host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
|
||||
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
|
||||
database: [{ required: true, message: '请输入数据库名', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const handleTypeChange = () => {
|
||||
// 根据类型设置默认端口
|
||||
if (form.type === 'mongodb') {
|
||||
form.port = 27017
|
||||
form.auth_source = 'admin'
|
||||
} else if (form.type === 'mysql') {
|
||||
form.port = 3306
|
||||
form.auth_source = undefined
|
||||
} else if (form.type === 'postgresql') {
|
||||
form.port = 5432
|
||||
form.auth_source = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
testing.value = true
|
||||
try {
|
||||
const connected = await store.testConnection({
|
||||
type: form.type!,
|
||||
host: form.host!,
|
||||
port: form.port!,
|
||||
database: form.database!,
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
auth_source: form.auth_source,
|
||||
})
|
||||
|
||||
// 统一在这里显示成功/失败消息
|
||||
if (connected) {
|
||||
ElMessage.success('连接测试成功')
|
||||
} else {
|
||||
ElMessage.error('连接测试失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
// store 内部已经显示了错误消息,这里不需要重复显示
|
||||
console.error('连接测试失败:', error)
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
// 如果是编辑且密码为空,则不提交密码字段
|
||||
if (isEdit.value && !formData.password) {
|
||||
delete formData.password
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await store.updateDataSource(route.params.id as string, formData)
|
||||
} else {
|
||||
await store.createDataSource(formData)
|
||||
}
|
||||
|
||||
router.push('/data-sources')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadDataSourceDetail = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const dataSource = await store.fetchDataSourceDetail(route.params.id as string)
|
||||
if (dataSource) {
|
||||
Object.assign(form, {
|
||||
name: dataSource.name,
|
||||
type: dataSource.type,
|
||||
host: dataSource.host,
|
||||
port: dataSource.port,
|
||||
database: dataSource.database,
|
||||
username: dataSource.username || '',
|
||||
password: '', // 不加载密码
|
||||
auth_source: dataSource.auth_source || 'admin',
|
||||
description: dataSource.description || '',
|
||||
is_tag_engine: dataSource.is_tag_engine || false,
|
||||
status: dataSource.status,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载数据源详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) {
|
||||
loadDataSourceDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.data-source-form {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
189
Moncter/TaskShow/src/views/DataSource/List.vue
Normal file
189
Moncter/TaskShow/src/views/DataSource/List.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="data-source-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据源列表</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加数据源
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="filters.type" placeholder="请选择类型" clearable @change="loadDataSources">
|
||||
<el-option label="MongoDB" value="mongodb" />
|
||||
<el-option label="MySQL" value="mysql" />
|
||||
<el-option label="PostgreSQL" value="postgresql" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="请选择状态" clearable @change="loadDataSources">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="filters.name" placeholder="请输入名称" clearable @change="loadDataSources" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadDataSources">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 数据源列表 -->
|
||||
<el-table :data="dataSources" border v-loading="loading">
|
||||
<el-table-column prop="name" label="名称" width="200" />
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.type === 'mongodb'" type="success">MongoDB</el-tag>
|
||||
<el-tag v-else-if="row.type === 'mysql'" type="primary">MySQL</el-tag>
|
||||
<el-tag v-else-if="row.type === 'postgresql'" type="info">PostgreSQL</el-tag>
|
||||
<el-tag v-else>{{ row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="host" label="主机" width="150" />
|
||||
<el-table-column prop="port" label="端口" width="100" />
|
||||
<el-table-column prop="database" label="数据库" width="150" />
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_tag_engine" label="标签引擎" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_tag_engine" type="warning">是</el-tag>
|
||||
<el-tag v-else type="info">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadDataSources"
|
||||
@current-change="loadDataSources"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useDataSourceStore } from '@/store/dataSource'
|
||||
import type { DataSource } from '@/api/dataSource'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useDataSourceStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSources = ref<DataSource[]>([])
|
||||
|
||||
const filters = reactive({
|
||||
type: '',
|
||||
status: undefined as number | undefined,
|
||||
name: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const loadDataSources = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await store.fetchDataSources({
|
||||
type: filters.type || undefined,
|
||||
status: filters.status,
|
||||
name: filters.name || undefined,
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
})
|
||||
dataSources.value = result.data_sources
|
||||
pagination.total = result.total
|
||||
} catch (error) {
|
||||
console.error('加载数据源列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push('/data-sources/create')
|
||||
}
|
||||
|
||||
const handleEdit = (row: DataSource) => {
|
||||
router.push(`/data-sources/${row.data_source_id}/edit`)
|
||||
}
|
||||
|
||||
const handleDelete = async (row: DataSource) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除数据源 "${row.name}" 吗?`, '提示', {
|
||||
type: 'warning',
|
||||
})
|
||||
await store.deleteDataSource(row.data_source_id)
|
||||
await loadDataSources()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除数据源失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.type = ''
|
||||
filters.status = undefined
|
||||
filters.name = ''
|
||||
pagination.page = 1
|
||||
loadDataSources()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDataSources()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.data-source-list {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
75
Moncter/TaskShow/src/views/Home.vue
Normal file
75
Moncter/TaskShow/src/views/Home.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<h1>Task Show</h1>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>欢迎使用</span>
|
||||
</div>
|
||||
</template>
|
||||
<p>这是一个基于 Vue3 + Element Plus + Pinia + TypeScript + Axios 的基础工程</p>
|
||||
<el-button type="primary" @click="handleTestRequest">测试请求</el-button>
|
||||
</el-card>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化 token
|
||||
userStore.initToken()
|
||||
})
|
||||
|
||||
const handleTestRequest = async () => {
|
||||
try {
|
||||
// 示例:测试 GET 请求
|
||||
const response = await request.get('/test', { id: 1 })
|
||||
ElMessage.success('请求成功: ' + JSON.stringify(response))
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.el-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
221
Moncter/TaskShow/src/views/TagDataList/Form.vue
Normal file
221
Moncter/TaskShow/src/views/TagDataList/Form.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="tag-data-list-form">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ isEdit ? '编辑数据列表' : '创建数据列表' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider>基本信息</el-divider>
|
||||
<el-form-item label="列表名称" prop="list_name">
|
||||
<el-input v-model="form.list_name" placeholder="例如: 消费记录表" />
|
||||
<div class="form-tip">数据列表的显示名称</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="列表编码" prop="list_code">
|
||||
<el-input v-model="form.list_code" placeholder="例如: consumption_records" :disabled="isEdit" />
|
||||
<div class="form-tip">唯一标识,创建后不可修改</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入数据列表描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 查询配置 -->
|
||||
<el-divider>查询配置</el-divider>
|
||||
<div class="form-tip" style="margin-bottom: 20px;">
|
||||
通过可视化界面配置MongoDB查询,支持过滤条件、联表查询、排序等功能
|
||||
</div>
|
||||
|
||||
<QueryBuilder v-model="queryConfig" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item style="margin-top: 30px;">
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import QueryBuilder from '@/components/QueryBuilder/QueryBuilder.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
list_name: '',
|
||||
list_code: '',
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 查询配置
|
||||
const queryConfig = reactive({
|
||||
data_source_id: '',
|
||||
database: '',
|
||||
collection: '',
|
||||
filter: [],
|
||||
lookups: [],
|
||||
sort_field: '',
|
||||
sort_order: '1',
|
||||
limit: 1000
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rules = {
|
||||
list_name: [{ required: true, message: '请输入列表名称', trigger: 'blur' }],
|
||||
list_code: [{ required: true, message: '请输入列表编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 验证查询配置
|
||||
const hasCollection = queryConfig.multi_collection
|
||||
? (queryConfig.collections && queryConfig.collections.length > 0)
|
||||
: queryConfig.collection
|
||||
|
||||
if (!queryConfig.data_source_id || !queryConfig.database || !hasCollection) {
|
||||
ElMessage.warning('请完成查询配置(数据源、数据库、集合)')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const submitData = {
|
||||
list_name: form.list_name,
|
||||
list_code: form.list_code,
|
||||
description: form.description || undefined,
|
||||
status: form.status,
|
||||
data_source_id: queryConfig.data_source_id,
|
||||
database: queryConfig.database,
|
||||
collection: queryConfig.multi_collection ? (queryConfig.collections[0] || '') : queryConfig.collection,
|
||||
multi_collection: queryConfig.multi_collection,
|
||||
collections: queryConfig.multi_collection ? queryConfig.collections : undefined,
|
||||
query_config: {
|
||||
filter: queryConfig.filter,
|
||||
lookups: queryConfig.lookups,
|
||||
sort: queryConfig.sort_field ? {
|
||||
[queryConfig.sort_field]: parseInt(queryConfig.sort_order)
|
||||
} : undefined,
|
||||
limit: queryConfig.limit
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 替换为真实API
|
||||
// if (isEdit.value) {
|
||||
// await request.put(`/tag-data-lists/${route.params.id}`, submitData)
|
||||
// } else {
|
||||
// await request.post('/tag-data-lists', submitData)
|
||||
// }
|
||||
|
||||
// Mock保存
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
console.log('保存的数据:', submitData)
|
||||
ElMessage.success(isEdit.value ? '数据列表更新成功(Mock)' : '数据列表创建成功(Mock)')
|
||||
|
||||
router.push('/tag-data-lists')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 加载详情
|
||||
const loadDetail = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const response = await request.get(`/tag-data-lists/${route.params.id}`)
|
||||
if (response.code === 200) {
|
||||
const data = response.data
|
||||
Object.assign(form, {
|
||||
list_name: data.list_name || '',
|
||||
list_code: data.list_code || '',
|
||||
description: data.description || '',
|
||||
status: data.status ?? 1
|
||||
})
|
||||
|
||||
// 加载查询配置
|
||||
if (data.query_config) {
|
||||
Object.assign(queryConfig, {
|
||||
data_source_id: data.data_source_id || '',
|
||||
database: data.database || '',
|
||||
collection: data.collection || '',
|
||||
multi_collection: data.multi_collection || false,
|
||||
collections: data.collections || [],
|
||||
filter: data.query_config.filter || [],
|
||||
lookups: data.query_config.lookups || [],
|
||||
sort_field: data.query_config.sort ? Object.keys(data.query_config.sort)[0] : '',
|
||||
sort_order: data.query_config.sort ? String(Object.values(data.query_config.sort)[0]) : '1',
|
||||
limit: data.query_config.limit || 1000
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) {
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-data-list-form {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
Moncter/TaskShow/src/views/TagDataList/List.vue
Normal file
244
Moncter/TaskShow/src/views/TagDataList/List.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="tag-data-list-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据列表管理</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建数据列表
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="列表名称">
|
||||
<el-input
|
||||
v-model="searchForm.list_name"
|
||||
placeholder="请输入列表名称"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable @change="handleSearch">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="dataList" v-loading="loading" border>
|
||||
<el-table-column prop="list_name" label="列表名称" min-width="150" />
|
||||
<el-table-column prop="list_code" label="列表编码" width="180" />
|
||||
<el-table-column prop="data_source_id" label="数据源ID" width="150" />
|
||||
<el-table-column prop="database" label="数据库" width="120" />
|
||||
<el-table-column prop="collection" label="主集合" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
|
||||
<el-tag v-else type="info">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="create_time" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const dataList = ref<any[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
list_name: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// TODO: 替换为真实API
|
||||
// const response = await request.get('/tag-data-lists', params)
|
||||
|
||||
// Mock数据
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
const mockData = [
|
||||
{
|
||||
list_id: 'list_001',
|
||||
list_code: 'consumption_records',
|
||||
list_name: '消费记录表',
|
||||
data_source_id: 'source_001',
|
||||
database: 'tag_engine',
|
||||
collection: 'consumption_records',
|
||||
description: '用于标签定义的消费记录数据',
|
||||
status: 1,
|
||||
create_time: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
list_id: 'list_002',
|
||||
list_code: 'user_profile',
|
||||
list_name: '用户档案表',
|
||||
data_source_id: 'source_001',
|
||||
database: 'tag_engine',
|
||||
collection: 'user_profile',
|
||||
description: '用户基本信息和统计数据',
|
||||
status: 1,
|
||||
create_time: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
// 应用筛选
|
||||
let filteredData = mockData
|
||||
if (searchForm.list_name) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.list_name.includes(searchForm.list_name)
|
||||
)
|
||||
}
|
||||
if (searchForm.status !== undefined) {
|
||||
filteredData = filteredData.filter(item => item.status === searchForm.status)
|
||||
}
|
||||
|
||||
dataList.value = filteredData
|
||||
pagination.total = filteredData.length
|
||||
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.list_name = ''
|
||||
searchForm.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 创建
|
||||
const handleCreate = () => {
|
||||
router.push('/tag-data-lists/create')
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row: any) => {
|
||||
router.push(`/tag-data-lists/${row.list_id}/edit`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除数据列表"${row.list_name}"吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: 替换为真实API
|
||||
// await request.delete(`/tag-data-lists/${row.list_id}`)
|
||||
|
||||
// Mock删除
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
ElMessage.success('删除成功(Mock)')
|
||||
loadData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-data-list-page {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
191
Moncter/TaskShow/src/views/TagDataList/README.md
Normal file
191
Moncter/TaskShow/src/views/TagDataList/README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 数据列表管理界面
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `List.vue` - 数据列表管理列表页面
|
||||
- `Form.vue` - 数据列表配置表单页面
|
||||
|
||||
## 功能说明
|
||||
|
||||
### List.vue(列表页面)
|
||||
|
||||
**功能**:
|
||||
- 显示所有数据列表配置
|
||||
- 搜索和筛选数据列表
|
||||
- 创建、编辑、删除数据列表
|
||||
- 分页展示
|
||||
|
||||
**Mock数据**:
|
||||
- 默认显示2条示例数据
|
||||
- 支持按名称和状态筛选
|
||||
|
||||
### Form.vue(配置表单页面)
|
||||
|
||||
**功能**:
|
||||
- 创建/编辑数据列表配置
|
||||
- 基本信息配置(名称、编码、描述、状态)
|
||||
- 使用QueryBuilder组件配置查询
|
||||
|
||||
**配置项**:
|
||||
1. **基本信息**
|
||||
- 列表名称(必填)
|
||||
- 列表编码(必填,创建后不可修改)
|
||||
- 描述(可选)
|
||||
- 状态(启用/禁用)
|
||||
|
||||
2. **查询配置**(通过QueryBuilder组件)
|
||||
- 数据源选择
|
||||
- 数据库选择
|
||||
- 主集合选择
|
||||
- 过滤条件配置
|
||||
- 联表查询配置
|
||||
- 排序和限制配置
|
||||
- 查询预览
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 访问 `/tag-data-lists` 查看列表
|
||||
2. 点击"创建数据列表"进入配置页面
|
||||
3. 填写基本信息
|
||||
4. 使用QueryBuilder配置查询:
|
||||
- 选择数据源、数据库、集合
|
||||
- 添加过滤条件(WHERE)
|
||||
- 添加联表查询(JOIN)
|
||||
- 配置排序和限制
|
||||
- 点击"预览数据"查看效果
|
||||
5. 保存配置
|
||||
|
||||
## Mock数据说明
|
||||
|
||||
当前所有API都使用Mock数据,标记为 `TODO: 替换为真实API`。
|
||||
|
||||
### List.vue Mock数据
|
||||
|
||||
```javascript
|
||||
// 默认2条数据列表
|
||||
[
|
||||
{
|
||||
list_id: 'list_001',
|
||||
list_name: '消费记录表',
|
||||
collection: 'consumption_records'
|
||||
},
|
||||
{
|
||||
list_id: 'list_002',
|
||||
list_name: '用户档案表',
|
||||
collection: 'user_profile'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### QueryBuilder Mock数据
|
||||
|
||||
```javascript
|
||||
// 数据源
|
||||
dataSources = [{ data_source_id: 'source_001', name: 'MongoDB标签引擎数据源' }]
|
||||
|
||||
// 数据库
|
||||
databases = ['tag_engine', 'business_db', 'analytics_db']
|
||||
|
||||
// 集合(根据数据库动态变化)
|
||||
collections = ['consumption_records', 'user_profile', 'user_phone_relations', ...]
|
||||
|
||||
// 字段(根据集合动态变化)
|
||||
// consumption_records: 交易金额、店铺名称、交易状态、手机号、用户ID
|
||||
// user_profile: 用户ID、总消费金额、总消费次数、最后消费时间
|
||||
|
||||
// 预览数据
|
||||
previewData = [3条示例消费记录]
|
||||
```
|
||||
|
||||
### TagDefinition/Form.vue Mock数据
|
||||
|
||||
```javascript
|
||||
// 数据列表下拉
|
||||
dataLists = [
|
||||
{ list_id: 'list_001', list_name: '消费记录表' },
|
||||
{ list_id: 'list_002', list_name: '用户档案表' }
|
||||
]
|
||||
|
||||
// 字段列表(根据选择的数据列表动态变化)
|
||||
```
|
||||
|
||||
## 界面展示效果
|
||||
|
||||
### 数据列表管理(List.vue)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 数据列表管理 [创建数据列表] │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 搜索:[列表名称] [状态] [查询] [重置] │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 列表名称 | 列表编码 | 数据源 | 数据库 | 状态 | 操作 │
|
||||
│ 消费记录表 | consumption_records | ... | 启用 | 编辑 删除│
|
||||
│ 用户档案表 | user_profile | ... | 启用 | 编辑 删除│
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据列表配置(Form.vue)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 创建数据列表 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ ══ 基本信息 ══ │
|
||||
│ 列表名称:[消费记录表] │
|
||||
│ 列表编码:[consumption_records] │
|
||||
│ 描述:[用于标签定义的消费记录数据] │
|
||||
│ 状态:⦿ 启用 ○ 禁用 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ ══ 查询配置 ══ │
|
||||
│ ┌─ 基础配置 ─────────────────────────────────────┐ │
|
||||
│ │ 数据源:[MongoDB数据源] │ │
|
||||
│ │ 数据库:[tag_engine] │ │
|
||||
│ │ 主集合:[consumption_records] │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 过滤条件(WHERE) ────────────── [添加条件] ──┐ │
|
||||
│ │ 逻辑 | 字段 | 运算符 | 值 | 操作 │ │
|
||||
│ │ - | 交易状态 | 等于 | success | 删除 │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 联表查询(JOIN) ───────────── [添加关联] ───┐ │
|
||||
│ │ 关联集合 | 主字段 | 关联字段 | 结果名 | 操作 │ │
|
||||
│ │ user_profile | user_id | user_id | user_info | 删除│ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 排序和限制 ────────────────────────────────────┐│
|
||||
│ │ 排序字段:[create_time] 排序方式:[降序] ││
|
||||
│ │ 限制数量:[1000] ││
|
||||
│ └─────────────────────────────────────────────────┘│
|
||||
│ ┌─ 查询预览 ─────────────────── [预览数据] ─────┐│
|
||||
│ │ db.consumption_records.aggregate([ ││
|
||||
│ │ { $match: { status: { $eq: "success" } } }, ││
|
||||
│ │ { $lookup: { ... } }, ││
|
||||
│ │ { $sort: { create_time: -1 } }, ││
|
||||
│ │ { $limit: 1000 } ││
|
||||
│ │ ]) ││
|
||||
│ │ ││
|
||||
│ │ [预览数据表格] ││
|
||||
│ └─────────────────────────────────────────────────┘│
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ [保存] [取消] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## API接口(待开发)
|
||||
|
||||
```
|
||||
GET /api/tag-data-lists - 获取数据列表列表
|
||||
POST /api/tag-data-lists - 创建数据列表
|
||||
GET /api/tag-data-lists/{id} - 获取数据列表详情
|
||||
PUT /api/tag-data-lists/{id} - 更新数据列表
|
||||
DELETE /api/tag-data-lists/{id} - 删除数据列表
|
||||
GET /api/tag-data-lists/{id}/fields - 获取数据列表字段
|
||||
|
||||
GET /api/data-sources/{id}/databases - 获取数据库列表
|
||||
GET /api/data-sources/{id}/collections - 获取集合列表
|
||||
GET /api/data-sources/{id}/fields - 获取字段列表
|
||||
POST /api/data-sources/preview-query - 预览查询结果
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有API调用都标记了 `TODO: 替换为真实API`
|
||||
2. 保存时会在控制台打印提交的数据,方便调试
|
||||
3. 界面完全可用,等后端API开发完成后,只需删除Mock代码即可
|
||||
130
Moncter/TaskShow/src/views/TagDefinition/Detail.vue
Normal file
130
Moncter/TaskShow/src/views/TagDefinition/Detail.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="tag-definition-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签定义详情</span>
|
||||
<el-button type="primary" @click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions title="基本信息" :column="2" border>
|
||||
<el-descriptions-item label="标签编码">{{ tag?.tag_code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标签名称">{{ tag?.tag_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">{{ tag?.category }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规则类型">
|
||||
<el-tag type="primary">{{ tag?.rule_type }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新频率">
|
||||
<el-tag v-if="tag?.update_frequency === 'real_time'" type="success">实时</el-tag>
|
||||
<el-tag v-else>{{ tag?.update_frequency }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag v-if="tag?.status === 0" type="success">启用</el-tag>
|
||||
<el-tag v-else type="info">禁用</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ tag?.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 规则配置 -->
|
||||
<div class="rule-config-section" v-if="tag?.rule_config">
|
||||
<h3>规则配置</h3>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="规则类型">
|
||||
{{ tag.rule_config.rule_type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标签值">
|
||||
{{ tag.rule_config.tag_value }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="置信度">
|
||||
{{ tag.rule_config.confidence }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin-top: 20px; margin-bottom: 10px;">规则条件</h4>
|
||||
<el-table :data="tag.rule_config.conditions" border>
|
||||
<el-table-column prop="field" label="字段" />
|
||||
<el-table-column prop="operator" label="运算符" />
|
||||
<el-table-column prop="value" label="值" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { TagDefinition } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const tag = ref<TagDefinition | null>(null)
|
||||
|
||||
const loadTagDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 调用API加载标签详情
|
||||
// const response = await request.get(`/tag-definitions/${route.params.id}`)
|
||||
// tag.value = response.data
|
||||
|
||||
// 模拟数据
|
||||
tag.value = {
|
||||
tag_id: route.params.id as string,
|
||||
tag_code: 'high_consumer',
|
||||
tag_name: '高消费用户',
|
||||
category: '消费能力',
|
||||
description: '总消费金额大于等于5000的用户',
|
||||
rule_type: 'simple',
|
||||
rule_config: {
|
||||
rule_type: 'simple',
|
||||
conditions: [
|
||||
{ field: 'total_amount', operator: '>=', value: 5000 }
|
||||
],
|
||||
tag_value: 'high',
|
||||
confidence: 1.0
|
||||
},
|
||||
update_frequency: 'real_time',
|
||||
status: 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载标签详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/tag-definitions/${route.params.id}/edit`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTagDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-definition-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-config-section {
|
||||
margin-top: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
543
Moncter/TaskShow/src/views/TagDefinition/Form.vue
Normal file
543
Moncter/TaskShow/src/views/TagDefinition/Form.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<div class="tag-definition-form">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ isEdit ? '编辑标签定义' : '创建标签定义' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider>基本信息</el-divider>
|
||||
<el-form-item label="标签编码" prop="tag_code">
|
||||
<el-input v-model="form.tag_code" placeholder="例如: consumer_level" :disabled="isEdit" />
|
||||
<div class="form-tip">唯一标识,创建后不可修改</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="tag_name">
|
||||
<el-input v-model="form.tag_name" placeholder="例如: 消费等级" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select v-model="form.category" placeholder="请选择分类(可选)" clearable>
|
||||
<el-option label="消费能力" value="消费能力" />
|
||||
<el-option label="活跃度" value="活跃度" />
|
||||
<el-option label="风险等级" value="风险等级" />
|
||||
<el-option label="生命周期" value="生命周期" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入标签描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="更新频率" prop="update_frequency">
|
||||
<el-select v-model="form.update_frequency" placeholder="请选择更新频率(可选)" clearable>
|
||||
<el-option label="实时" value="real_time" />
|
||||
<el-option label="每日" value="daily" />
|
||||
<el-option label="每周" value="weekly" />
|
||||
<el-option label="每月" value="monthly" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="0">启用</el-radio>
|
||||
<el-radio :label="1">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 规则配置 -->
|
||||
<el-divider>规则配置</el-divider>
|
||||
|
||||
<!-- 数据列表选择 -->
|
||||
<el-form-item label="数据列表" prop="data_list_id">
|
||||
<el-select
|
||||
v-model="form.data_list_id"
|
||||
placeholder="请选择数据列表"
|
||||
filterable
|
||||
@change="handleDataListChange"
|
||||
:loading="dataListLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="list in dataLists"
|
||||
:key="list.list_id"
|
||||
:label="list.list_name"
|
||||
:value="list.list_id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">选择数据列表后,将显示该列表的字段供选择</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 规则类型选择 -->
|
||||
<el-form-item label="规则类型" prop="rule_type">
|
||||
<el-radio-group v-model="form.rule_type" @change="handleRuleTypeChange">
|
||||
<el-radio label="simple">运算规则</el-radio>
|
||||
<el-radio label="regex">正则规则</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">
|
||||
<span v-if="form.rule_type === 'simple'">运算规则:使用运算符比较字段值</span>
|
||||
<span v-else>正则规则:使用正则表达式匹配字符串字段</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 规则条件 -->
|
||||
<el-form-item label="规则条件" prop="conditions">
|
||||
<el-button type="primary" @click="handleAddCondition" :disabled="!form.data_list_id">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加条件
|
||||
</el-button>
|
||||
<div v-if="!form.data_list_id" class="form-tip" style="color: #f56c6c;">
|
||||
请先选择数据列表
|
||||
</div>
|
||||
<el-table
|
||||
:data="form.rule_config.conditions"
|
||||
border
|
||||
style="margin-top: 10px"
|
||||
v-if="form.rule_config.conditions && form.rule_config.conditions.length > 0"
|
||||
>
|
||||
<el-table-column label="字段" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.field"
|
||||
placeholder="请选择字段"
|
||||
@change="handleFieldChange(row)"
|
||||
:loading="fieldsLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="field in fields"
|
||||
:key="field.field_name"
|
||||
:label="field.field"
|
||||
:value="field.field"
|
||||
:disabled="field.type === 'object' || field.type === 'array'"
|
||||
>
|
||||
<span>{{ field.field }}</span>
|
||||
<span style="color: #909399; margin-left: 8px;">({{ field.type }})</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运算符" width="180">
|
||||
<template #default="{ row }">
|
||||
<!-- 运算规则运算符 -->
|
||||
<el-select
|
||||
v-if="form.rule_type === 'simple'"
|
||||
v-model="row.operator"
|
||||
placeholder="请选择运算符"
|
||||
>
|
||||
<el-option label="大于" value=">" />
|
||||
<el-option label="大于等于" value=">=" />
|
||||
<el-option label="小于" value="<" />
|
||||
<el-option label="小于等于" value="<=" />
|
||||
<el-option label="等于" value="=" />
|
||||
<el-option label="不等于" value="!=" />
|
||||
<el-option label="在列表中" value="in" />
|
||||
<el-option label="不在列表中" value="not_in" />
|
||||
</el-select>
|
||||
<!-- 正则规则运算符 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="row.operator"
|
||||
placeholder="例如: /淘宝/"
|
||||
>
|
||||
<template #prepend>正则</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="值" width="200">
|
||||
<template #default="{ row }">
|
||||
<!-- 运算规则值 -->
|
||||
<el-input-number
|
||||
v-if="form.rule_type === 'simple' && row.operator !== 'in' && row.operator !== 'not_in'"
|
||||
v-model="row.value"
|
||||
style="width: 100%"
|
||||
:precision="0"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="form.rule_type === 'simple'"
|
||||
v-model="row.value"
|
||||
placeholder='例如: ["值1","值2"]'
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
<!-- 正则规则值 -->
|
||||
<el-switch
|
||||
v-else
|
||||
v-model="row.value"
|
||||
active-text="匹配"
|
||||
inactive-text="不匹配"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签值" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.tag_value"
|
||||
placeholder="满足条件时的标签值"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleRemoveCondition($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="form.rule_config.conditions && form.rule_config.conditions.length > 0" class="form-tip">
|
||||
注意:一个item代表一个条件,满足任一条件即使用该条件的标签值
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useTagDefinitionStore } from '@/store'
|
||||
import { getTagDefinitionDetail, createTagDefinition, updateTagDefinition } from '@/api/tagDefinition'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const store = useTagDefinitionStore()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const submitting = ref(false)
|
||||
const dataListLoading = ref(false)
|
||||
const fieldsLoading = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataLists = ref<Array<{ list_id: string; list_name: string }>>([])
|
||||
const fields = ref<Array<{ field: string; field_name: string; type: string; description?: string }>>([])
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
tag_code: '',
|
||||
tag_name: '',
|
||||
category: '',
|
||||
description: '',
|
||||
rule_type: 'simple' as 'simple' | 'regex',
|
||||
data_list_id: '',
|
||||
data_list_name: '',
|
||||
rule_config: {
|
||||
rule_type: 'simple' as 'simple' | 'regex',
|
||||
data_list_id: '',
|
||||
data_list_name: '',
|
||||
conditions: [] as Array<{
|
||||
field: string
|
||||
field_name: string
|
||||
operator: string
|
||||
value: any
|
||||
tag_value: string
|
||||
}>
|
||||
},
|
||||
update_frequency: 'real_time',
|
||||
status: 0
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rules = {
|
||||
tag_code: [{ required: true, message: '请输入标签编码', trigger: 'blur' }],
|
||||
tag_name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }],
|
||||
data_list_id: [{ required: true, message: '请选择数据列表', trigger: 'change' }],
|
||||
conditions: [
|
||||
{
|
||||
validator: (rule: any, value: any[], callback: Function) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('至少添加一个规则条件'))
|
||||
} else {
|
||||
// 验证每个条件
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const condition = value[i]
|
||||
if (!condition.field) {
|
||||
callback(new Error(`第${i + 1}个条件:请选择字段`))
|
||||
return
|
||||
}
|
||||
if (!condition.operator) {
|
||||
callback(new Error(`第${i + 1}个条件:请选择运算符`))
|
||||
return
|
||||
}
|
||||
if (condition.value === undefined || condition.value === null || condition.value === '') {
|
||||
callback(new Error(`第${i + 1}个条件:请输入值`))
|
||||
return
|
||||
}
|
||||
if (!condition.tag_value) {
|
||||
callback(new Error(`第${i + 1}个条件:请输入标签值`))
|
||||
return
|
||||
}
|
||||
}
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 加载数据列表
|
||||
const loadDataLists = async () => {
|
||||
try {
|
||||
dataListLoading.value = true
|
||||
|
||||
// TODO: 替换为真实API
|
||||
// const response = await request.get('/tag-data-lists', { status: 1 })
|
||||
|
||||
// Mock数据
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
dataLists.value = [
|
||||
{
|
||||
list_id: 'list_001',
|
||||
list_name: '消费记录表'
|
||||
},
|
||||
{
|
||||
list_id: 'list_002',
|
||||
list_name: '用户档案表'
|
||||
}
|
||||
]
|
||||
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载数据列表失败')
|
||||
} finally {
|
||||
dataListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数据列表变化
|
||||
const handleDataListChange = async (listId: string) => {
|
||||
if (!listId) {
|
||||
fields.value = []
|
||||
form.data_list_name = ''
|
||||
form.rule_config.data_list_id = ''
|
||||
form.rule_config.data_list_name = ''
|
||||
form.rule_config.conditions = []
|
||||
return
|
||||
}
|
||||
|
||||
const selectedList = dataLists.value.find(l => l.list_id === listId)
|
||||
if (selectedList) {
|
||||
form.data_list_name = selectedList.list_name
|
||||
form.rule_config.data_list_id = listId
|
||||
form.rule_config.data_list_name = selectedList.list_name
|
||||
}
|
||||
|
||||
// 加载字段列表
|
||||
await loadFields(listId)
|
||||
}
|
||||
|
||||
// 加载字段列表
|
||||
const loadFields = async (listId: string) => {
|
||||
try {
|
||||
fieldsLoading.value = true
|
||||
|
||||
// TODO: 替换为真实API
|
||||
// const response = await request.get(`/tag-data-lists/${listId}/fields`)
|
||||
|
||||
// Mock数据
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
if (listId === 'list_001') {
|
||||
// 消费记录表字段
|
||||
fields.value = [
|
||||
{ field: '交易金额', field_name: 'amount', type: 'number', description: '交易金额' },
|
||||
{ field: '店铺名称', field_name: 'shop_name', type: 'string', description: '店铺名称' },
|
||||
{ field: '交易状态', field_name: 'status', type: 'string', description: '交易状态' },
|
||||
{ field: '手机号', field_name: 'phone', type: 'string', description: '手机号' },
|
||||
{ field: '用户ID', field_name: 'user_id', type: 'string', description: '用户ID' }
|
||||
]
|
||||
} else if (listId === 'list_002') {
|
||||
// 用户档案表字段
|
||||
fields.value = [
|
||||
{ field: '用户ID', field_name: 'user_id', type: 'string', description: '用户ID' },
|
||||
{ field: '总消费金额', field_name: 'total_amount', type: 'number', description: '总消费金额' },
|
||||
{ field: '总消费次数', field_name: 'total_count', type: 'number', description: '总消费次数' },
|
||||
{ field: '最后消费时间', field_name: 'last_consume_time', type: 'datetime', description: '最后消费时间' }
|
||||
]
|
||||
} else {
|
||||
fields.value = []
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载字段列表失败')
|
||||
fields.value = []
|
||||
} finally {
|
||||
fieldsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字段变化
|
||||
const handleFieldChange = (row: any) => {
|
||||
const field = fields.value.find(f => f.field === row.field)
|
||||
if (field) {
|
||||
row.field_name = field.field_name
|
||||
}
|
||||
}
|
||||
|
||||
// 规则类型变化
|
||||
const handleRuleTypeChange = (type: string) => {
|
||||
form.rule_config.rule_type = type as 'simple' | 'regex'
|
||||
// 重置所有条件
|
||||
form.rule_config.conditions.forEach(condition => {
|
||||
condition.operator = ''
|
||||
condition.value = type === 'regex' ? true : ''
|
||||
})
|
||||
}
|
||||
|
||||
// 添加条件
|
||||
const handleAddCondition = () => {
|
||||
if (!form.data_list_id) {
|
||||
ElMessage.warning('请先选择数据列表')
|
||||
return
|
||||
}
|
||||
|
||||
form.rule_config.conditions = form.rule_config.conditions || []
|
||||
form.rule_config.conditions.push({
|
||||
field: '',
|
||||
field_name: '',
|
||||
operator: form.rule_type === 'regex' ? '/淘宝/' : '>=',
|
||||
value: form.rule_type === 'regex' ? true : '',
|
||||
tag_value: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除条件
|
||||
const handleRemoveCondition = (index: number) => {
|
||||
form.rule_config.conditions?.splice(index, 1)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
// 准备提交数据
|
||||
const submitData = {
|
||||
tag_code: form.tag_code,
|
||||
tag_name: form.tag_name,
|
||||
category: form.category || undefined,
|
||||
description: form.description || undefined,
|
||||
rule_type: form.rule_type,
|
||||
rule_config: {
|
||||
rule_type: form.rule_type,
|
||||
data_list_id: form.data_list_id,
|
||||
data_list_name: form.data_list_name,
|
||||
conditions: form.rule_config.conditions.map(condition => ({
|
||||
field: condition.field,
|
||||
field_name: condition.field_name,
|
||||
operator: condition.operator,
|
||||
value: condition.value,
|
||||
tag_value: condition.tag_value
|
||||
}))
|
||||
},
|
||||
update_frequency: form.update_frequency || undefined,
|
||||
status: form.status
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateTagDefinition(route.params.id as string, submitData)
|
||||
ElMessage.success('标签定义更新成功')
|
||||
} else {
|
||||
await createTagDefinition(submitData)
|
||||
ElMessage.success('标签定义创建成功')
|
||||
}
|
||||
router.push('/tag-definitions')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 加载标签详情
|
||||
const loadTagDetail = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const tag = await getTagDefinitionDetail(route.params.id as string)
|
||||
if (tag) {
|
||||
Object.assign(form, {
|
||||
tag_code: tag.tag_code,
|
||||
tag_name: tag.tag_name,
|
||||
category: tag.category || '',
|
||||
description: tag.description || '',
|
||||
rule_type: tag.rule_type || 'simple',
|
||||
update_frequency: tag.update_frequency || 'real_time',
|
||||
status: tag.status
|
||||
})
|
||||
|
||||
// 设置规则配置
|
||||
if (tag.rule_config) {
|
||||
form.rule_config = {
|
||||
rule_type: tag.rule_config.rule_type || tag.rule_type || 'simple',
|
||||
data_list_id: tag.rule_config.data_list_id || '',
|
||||
data_list_name: tag.rule_config.data_list_name || '',
|
||||
conditions: tag.rule_config.conditions || []
|
||||
}
|
||||
form.data_list_id = form.rule_config.data_list_id
|
||||
form.data_list_name = form.rule_config.data_list_name
|
||||
|
||||
// 加载字段列表
|
||||
if (form.data_list_id) {
|
||||
await loadFields(form.data_list_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载标签详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDataLists()
|
||||
if (isEdit.value) {
|
||||
await loadTagDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-definition-form {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
239
Moncter/TaskShow/src/views/TagDefinition/List.vue
Normal file
239
Moncter/TaskShow/src/views/TagDefinition/List.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="tag-definition-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签定义列表</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建标签
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="标签名称">
|
||||
<el-input
|
||||
v-model="filters.name"
|
||||
placeholder="请输入标签名称"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select
|
||||
v-model="filters.category"
|
||||
placeholder="请选择分类"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="消费能力" value="消费能力" />
|
||||
<el-option label="活跃度" value="活跃度" />
|
||||
<el-option label="风险等级" value="风险等级" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="启用" :value="0" />
|
||||
<el-option label="禁用" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<el-table :data="tagList" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="tag_code" label="标签编码" width="180" />
|
||||
<el-table-column prop="tag_name" label="标签名称" min-width="150" />
|
||||
<el-table-column prop="category" label="分类" width="120" />
|
||||
<el-table-column prop="rule_type" label="规则类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.rule_type === 'simple'" type="primary">简单规则</el-tag>
|
||||
<el-tag v-else type="info">{{ row.rule_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="update_frequency" label="更新频率" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.update_frequency === 'real_time'" type="success">实时</el-tag>
|
||||
<el-tag v-else-if="row.update_frequency === 'daily'" type="warning">每日</el-tag>
|
||||
<el-tag v-else>{{ row.update_frequency }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 0" type="success">启用</el-tag>
|
||||
<el-tag v-else type="info">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
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">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useTagDefinitionStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import type { TagDefinition } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useTagDefinitionStore()
|
||||
|
||||
const filters = ref({
|
||||
name: '',
|
||||
category: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tagList = computed(() => store.definitions)
|
||||
const loading = computed(() => store.loading)
|
||||
|
||||
const loadTags = async () => {
|
||||
try {
|
||||
const result = await store.fetchDefinitions({
|
||||
...filters.value,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
pagination.value.total = result.total
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载标签列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.page = 1
|
||||
loadTags()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
name: '',
|
||||
category: '',
|
||||
status: undefined
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push('/tag-definitions/create')
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
router.push(`/tag-definitions/${row.tag_id}`)
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
router.push(`/tag-definitions/${row.tag_id}/edit`)
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该标签定义吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.deleteDefinition(row.tag_id)
|
||||
ElMessage.success('标签定义已删除')
|
||||
await loadTags()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除标签定义失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
loadTags()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadTags()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTags()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-definition-list {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
312
Moncter/TaskShow/src/views/TagFilter/index.vue
Normal file
312
Moncter/TaskShow/src/views/TagFilter/index.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div class="tag-filter">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签筛选</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 条件配置 -->
|
||||
<div class="conditions-section">
|
||||
<h3>筛选条件</h3>
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="逻辑关系">
|
||||
<el-radio-group v-model="logic">
|
||||
<el-radio label="AND">AND(所有条件都满足)</el-radio>
|
||||
<el-radio label="OR">OR(任一条件满足)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-button type="primary" @click="handleAddCondition" style="margin-bottom: 15px;">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加条件
|
||||
</el-button>
|
||||
|
||||
<el-table :data="conditions" border>
|
||||
<el-table-column label="标签" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.tag_code"
|
||||
placeholder="请选择标签"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="tag in tagDefinitions"
|
||||
:key="tag.tag_code"
|
||||
:label="tag.tag_name"
|
||||
:value="tag.tag_code"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运算符" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.operator">
|
||||
<el-option label="等于" value="eq" />
|
||||
<el-option label="不等于" value="ne" />
|
||||
<el-option label="大于" value="gt" />
|
||||
<el-option label="大于等于" value="gte" />
|
||||
<el-option label="小于" value="lt" />
|
||||
<el-option label="小于等于" value="lte" />
|
||||
<el-option label="在列表中" value="in" />
|
||||
<el-option label="不在列表中" value="nin" />
|
||||
<el-option label="包含" value="contains" />
|
||||
<el-option label="不包含" value="not_contains" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="值">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.value" placeholder="值" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleRemoveCondition($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="handleSearch" :loading="searching">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button type="success" @click="handleSaveCohort" :disabled="!hasResults">
|
||||
保存为人群快照
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示 -->
|
||||
<div class="results-section" v-if="hasResults">
|
||||
<h3>查询结果</h3>
|
||||
<div class="result-stats">
|
||||
<el-alert
|
||||
:title="`共找到 ${pagination.total} 个用户`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="results" border style="margin-top: 15px">
|
||||
<el-table-column prop="user_id" label="用户ID" width="200" />
|
||||
<el-table-column prop="name" label="姓名" width="120" />
|
||||
<el-table-column prop="phone" label="手机号" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ maskPhone(row.phone) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_amount" label="总消费金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.total_amount?.toFixed(2) || '0.00' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_count" label="消费次数" width="100" />
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="handleViewUser(row.user_id)">
|
||||
查看标签
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useTagDefinitionStore } from '@/store'
|
||||
import * as tagQueryApi from '@/api/tagQuery'
|
||||
import * as tagCohortApi from '@/api/tagCohort'
|
||||
import { maskPhone } from '@/utils'
|
||||
import type { TagCondition, UserInfo } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const tagDefinitionStore = useTagDefinitionStore()
|
||||
|
||||
const logic = ref<'AND' | 'OR'>('AND')
|
||||
const conditions = ref<TagCondition[]>([])
|
||||
const searching = ref(false)
|
||||
const results = ref<UserInfo[]>([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tagDefinitions = computed(() => tagDefinitionStore.definitions)
|
||||
const hasResults = computed(() => results.value.length > 0 || pagination.value.total > 0)
|
||||
|
||||
const loadTagDefinitions = async () => {
|
||||
try {
|
||||
await tagDefinitionStore.getActiveDefinitions()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载标签定义失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddCondition = () => {
|
||||
conditions.value.push({
|
||||
tag_code: '',
|
||||
operator: 'eq',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveCondition = (index: number) => {
|
||||
conditions.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (conditions.value.length === 0) {
|
||||
ElMessage.warning('请至少添加一个筛选条件')
|
||||
return
|
||||
}
|
||||
|
||||
searching.value = true
|
||||
try {
|
||||
const response = await tagQueryApi.filterUsersByTags({
|
||||
tag_conditions: conditions.value,
|
||||
logic: logic.value,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize,
|
||||
include_user_info: true
|
||||
})
|
||||
results.value = response.data.users
|
||||
pagination.value.total = response.data.total
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '查询失败')
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
conditions.value = []
|
||||
results.value = []
|
||||
pagination.value = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveCohort = async () => {
|
||||
try {
|
||||
const { value: name } = await ElMessageBox.prompt('请输入人群快照名称', '保存人群快照', {
|
||||
confirmButtonText: '保存',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: '快照名称不能为空'
|
||||
})
|
||||
|
||||
await tagCohortApi.createTagCohort({
|
||||
name: name,
|
||||
conditions: conditions.value,
|
||||
logic: logic.value,
|
||||
user_ids: results.value.map(u => u.user_id!)
|
||||
})
|
||||
|
||||
ElMessage.success('人群快照保存成功')
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '保存人群快照失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewUser = (userId: string) => {
|
||||
router.push(`/tag-query/user?user_id=${userId}`)
|
||||
}
|
||||
|
||||
// maskPhone 已从 utils 导入
|
||||
|
||||
const handleSizeChange = () => {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTagDefinitions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-filter {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conditions-section {
|
||||
margin-bottom: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
margin-top: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
193
Moncter/TaskShow/src/views/TagQuery/History.vue
Normal file
193
Moncter/TaskShow/src/views/TagQuery/History.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="tag-history">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签历史</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 查询表单 -->
|
||||
<el-form :inline="true" :model="queryForm" @submit.prevent="handleQuery">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input
|
||||
v-model="queryForm.user_id"
|
||||
placeholder="请输入用户ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select
|
||||
v-model="queryForm.tag_id"
|
||||
placeholder="请选择标签"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="tag in tagDefinitions"
|
||||
:key="tag.tag_id"
|
||||
:label="tag.tag_name"
|
||||
:value="tag.tag_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="queryForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 历史记录表格 -->
|
||||
<el-table :data="historyList" v-loading="loading" border style="margin-top: 20px">
|
||||
<el-table-column prop="user_id" label="用户ID" width="200" />
|
||||
<el-table-column prop="tag_name" label="标签名称" width="150" />
|
||||
<el-table-column prop="old_value" label="旧值" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.old_value">{{ row.old_value }}</span>
|
||||
<span v-else style="color: #909399;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="new_value" label="新值" width="150" />
|
||||
<el-table-column prop="change_reason" label="变更原因" width="150" />
|
||||
<el-table-column prop="change_time" label="变更时间" width="180" />
|
||||
<el-table-column prop="operator" label="操作人" width="120" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { TagDefinition } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const historyList = ref([])
|
||||
const tagDefinitions = ref<TagDefinition[]>([])
|
||||
|
||||
const queryForm = reactive({
|
||||
user_id: '',
|
||||
tag_id: '',
|
||||
dateRange: null as [Date, Date] | null
|
||||
})
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const loadTagDefinitions = async () => {
|
||||
try {
|
||||
// TODO: 调用API加载标签定义列表
|
||||
// const response = await request.get('/tag-definitions', { status: 0 })
|
||||
// tagDefinitions.value = response.data.definitions
|
||||
|
||||
// 模拟数据
|
||||
tagDefinitions.value = []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载标签定义失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 调用历史查询API
|
||||
// const response = await request.get('/tags/history', {
|
||||
// user_id: queryForm.user_id,
|
||||
// tag_id: queryForm.tag_id,
|
||||
// start_date: queryForm.dateRange?.[0],
|
||||
// end_date: queryForm.dateRange?.[1],
|
||||
// page: pagination.value.page,
|
||||
// page_size: pagination.value.pageSize
|
||||
// })
|
||||
// historyList.value = response.data.items
|
||||
// pagination.value.total = response.data.total
|
||||
|
||||
// 模拟数据
|
||||
historyList.value = []
|
||||
pagination.value.total = 0
|
||||
} catch (error) {
|
||||
ElMessage.error('查询历史记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryForm.user_id = ''
|
||||
queryForm.tag_id = ''
|
||||
queryForm.dateRange = null
|
||||
pagination.value.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTagDefinitions()
|
||||
|
||||
// 如果URL中有参数,自动查询
|
||||
const userId = route.query.user_id as string
|
||||
const tagId = route.query.tag_id as string
|
||||
if (userId) {
|
||||
queryForm.user_id = userId
|
||||
}
|
||||
if (tagId) {
|
||||
queryForm.tag_id = tagId
|
||||
}
|
||||
if (userId || tagId) {
|
||||
handleQuery()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-history {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
165
Moncter/TaskShow/src/views/TagQuery/Statistics.vue
Normal file
165
Moncter/TaskShow/src/views/TagQuery/Statistics.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="tag-statistics">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签统计</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<el-form :inline="true" :model="filters" style="margin-bottom: 20px;">
|
||||
<el-form-item label="标签">
|
||||
<el-select
|
||||
v-model="filters.tag_id"
|
||||
placeholder="请选择标签"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="tag in tagDefinitions"
|
||||
:key="tag.tag_id"
|
||||
:label="tag.tag_name"
|
||||
:value="tag.tag_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 统计图表 -->
|
||||
<div class="charts-section" v-if="hasData">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>标签值分布</span>
|
||||
</template>
|
||||
<div id="valueDistributionChart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>标签趋势</span>
|
||||
</template>
|
||||
<div id="trendChart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>标签覆盖度统计</span>
|
||||
</template>
|
||||
<el-table :data="coverageStats" border>
|
||||
<el-table-column prop="tag_name" label="标签名称" />
|
||||
<el-table-column prop="total_users" label="总用户数" />
|
||||
<el-table-column prop="tagged_users" label="已打标签用户数" />
|
||||
<el-table-column prop="coverage_rate" label="覆盖率">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.coverage_rate" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!hasData" description="请选择标签并查询统计数据" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { TagDefinition } from '@/types'
|
||||
// import * as echarts from 'echarts' // 需要安装 echarts
|
||||
|
||||
const tagDefinitions = ref<TagDefinition[]>([])
|
||||
const hasData = ref(false)
|
||||
const coverageStats = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
tag_id: '',
|
||||
dateRange: null as [Date, Date] | null
|
||||
})
|
||||
|
||||
const loadTagDefinitions = async () => {
|
||||
try {
|
||||
// TODO: 调用API加载标签定义列表
|
||||
// const response = await request.get('/tag-definitions', { status: 0 })
|
||||
// tagDefinitions.value = response.data.definitions
|
||||
|
||||
// 模拟数据
|
||||
tagDefinitions.value = []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载标签定义失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = async () => {
|
||||
if (!filters.tag_id) {
|
||||
ElMessage.warning('请选择标签')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: 调用统计API
|
||||
// const response = await request.get('/tags/statistics', {
|
||||
// tag_id: filters.tag_id,
|
||||
// start_date: filters.dateRange?.[0],
|
||||
// end_date: filters.dateRange?.[1]
|
||||
// })
|
||||
// 处理统计数据并渲染图表
|
||||
|
||||
hasData.value = true
|
||||
|
||||
// TODO: 初始化图表
|
||||
// initCharts(response.data)
|
||||
} catch (error) {
|
||||
ElMessage.error('查询统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.tag_id = ''
|
||||
filters.dateRange = null
|
||||
hasData.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTagDefinitions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-statistics {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
204
Moncter/TaskShow/src/views/TagQuery/User.vue
Normal file
204
Moncter/TaskShow/src/views/TagQuery/User.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="user-tag-query">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户标签查询</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 查询表单 -->
|
||||
<el-form :inline="true" :model="queryForm" @submit.prevent="handleQuery">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input
|
||||
v-model="queryForm.user_id"
|
||||
placeholder="请输入用户ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model="queryForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info" v-if="userInfo">
|
||||
<el-descriptions title="用户基本信息" :column="2" border>
|
||||
<el-descriptions-item label="用户ID">{{ userInfo.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ userInfo.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ maskPhone(userInfo.phone) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总消费金额">
|
||||
¥{{ userInfo.total_amount?.toFixed(2) || '0.00' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消费次数">{{ userInfo.total_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后消费时间">
|
||||
{{ userInfo.last_consume_time || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<div class="tags-section" v-if="tags.length > 0">
|
||||
<div class="section-header">
|
||||
<h3>用户标签</h3>
|
||||
<el-button type="primary" @click="handleRecalculate">
|
||||
重新计算标签
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="tags" border>
|
||||
<el-table-column prop="tag_name" label="标签名称" width="150" />
|
||||
<el-table-column prop="tag_code" label="标签编码" width="180" />
|
||||
<el-table-column prop="category" label="分类" width="120" />
|
||||
<el-table-column prop="tag_value" label="标签值" width="150" />
|
||||
<el-table-column prop="confidence" label="置信度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.confidence" :show-text="false" />
|
||||
<span style="margin-left: 10px;">{{ row.confidence }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="update_time" label="更新时间" width="180" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="handleViewHistory(row.tag_id)">
|
||||
查看历史
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && !userInfo && !queryForm.user_id && !queryForm.phone" description="请输入用户ID或手机号进行查询" />
|
||||
<el-empty v-if="!loading && userInfo && tags.length === 0" description="该用户暂无标签" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UserInfo, UserTag } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const tags = ref<UserTag[]>([])
|
||||
|
||||
const queryForm = reactive({
|
||||
user_id: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const handleQuery = async () => {
|
||||
if (!queryForm.user_id && !queryForm.phone) {
|
||||
ElMessage.warning('请输入用户ID或手机号')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 根据用户ID或手机号查询用户信息
|
||||
// 然后调用标签查询API
|
||||
// const response = await request.get(`/users/${userId}/tags`)
|
||||
// tags.value = response.data.tags
|
||||
|
||||
// 模拟数据
|
||||
userInfo.value = {
|
||||
user_id: queryForm.user_id || 'user-1',
|
||||
name: '张三',
|
||||
phone: '13800138000',
|
||||
total_amount: 5000,
|
||||
total_count: 10
|
||||
}
|
||||
tags.value = []
|
||||
} catch (error) {
|
||||
ElMessage.error('查询失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryForm.user_id = ''
|
||||
queryForm.phone = ''
|
||||
userInfo.value = null
|
||||
tags.value = []
|
||||
}
|
||||
|
||||
const handleRecalculate = async () => {
|
||||
if (!userInfo.value) return
|
||||
|
||||
try {
|
||||
// TODO: 调用重新计算API
|
||||
// await request.put(`/users/${userInfo.value.user_id}/tags`)
|
||||
ElMessage.success('标签重新计算成功')
|
||||
handleQuery()
|
||||
} catch (error) {
|
||||
ElMessage.error('重新计算失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewHistory = (tagId: string) => {
|
||||
router.push(`/tag-query/history?user_id=${userInfo.value?.user_id}&tag_id=${tagId}`)
|
||||
}
|
||||
|
||||
const maskPhone = (phone?: string) => {
|
||||
if (!phone) return '-'
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 如果URL中有user_id参数,自动查询
|
||||
const userId = route.query.user_id as string
|
||||
if (userId) {
|
||||
queryForm.user_id = userId
|
||||
handleQuery()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-tag-query {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
margin-top: 30px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
198
Moncter/TaskShow/src/views/TagTask/TaskDetail.vue
Normal file
198
Moncter/TaskShow/src/views/TagTask/TaskDetail.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="tag-task-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签任务详情</span>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="task?.status === 'pending' || task?.status === 'paused'"
|
||||
type="success"
|
||||
@click="handleStart"
|
||||
>
|
||||
启动
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="task?.status === 'running'"
|
||||
type="warning"
|
||||
@click="handlePause"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="task?.status === 'running' || task?.status === 'paused'"
|
||||
type="danger"
|
||||
@click="handleStop"
|
||||
>
|
||||
停止
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions title="基本信息" :column="2" border>
|
||||
<el-descriptions-item label="任务名称">{{ task?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务状态">
|
||||
<StatusBadge :status="task?.status || 'pending'" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务类型">
|
||||
<el-tag v-if="task?.task_type === 'full'" type="primary">全量计算</el-tag>
|
||||
<el-tag v-else-if="task?.task_type === 'incremental'" type="success">增量计算</el-tag>
|
||||
<el-tag v-else type="info">指定用户</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="目标标签数量">
|
||||
{{ task?.target_tag_ids?.length || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(task?.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(task?.updated_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务描述" :span="2">{{ task?.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div class="progress-section" v-if="task?.progress">
|
||||
<h3>进度信息</h3>
|
||||
<ProgressDisplay
|
||||
:title="'任务进度'"
|
||||
:percentage="task.progress.percentage || 0"
|
||||
:total="task.progress.total_users"
|
||||
:processed="task.progress.processed_users"
|
||||
:success="task.progress.success_count"
|
||||
:error="task.progress.error_count"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 执行记录 -->
|
||||
<div class="executions-section">
|
||||
<h3>执行记录</h3>
|
||||
<el-table :data="executions" border>
|
||||
<el-table-column prop="started_at" label="开始时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.started_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="finished_at" label="结束时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.finished_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<StatusBadge :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processed_users" label="处理用户数" width="120" />
|
||||
<el-table-column prop="success_count" label="成功数" width="100" />
|
||||
<el-table-column prop="error_count" label="失败数" width="100" />
|
||||
<el-table-column prop="error_message" label="错误信息" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import StatusBadge from '@/components/StatusBadge/index.vue'
|
||||
import ProgressDisplay from '@/components/ProgressDisplay/index.vue'
|
||||
import { useTagTaskStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useTagTaskStore()
|
||||
|
||||
const loading = computed(() => store.loading)
|
||||
const task = computed(() => store.currentTask)
|
||||
const executions = computed(() => store.executions)
|
||||
|
||||
const loadTaskDetail = async () => {
|
||||
try {
|
||||
await store.fetchTaskDetail(route.params.id as string)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadExecutions = async () => {
|
||||
try {
|
||||
await store.fetchExecutions(route.params.id as string)
|
||||
} catch (error) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
await store.startTask(route.params.id as string)
|
||||
ElMessage.success('任务已启动')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '启动任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
await store.pauseTask(route.params.id as string)
|
||||
ElMessage.success('任务已暂停')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '暂停任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要停止该任务吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.stopTask(route.params.id as string)
|
||||
ElMessage.success('任务已停止')
|
||||
await loadTaskDetail()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '停止任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/tag-tasks/${route.params.id}/edit`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTaskDetail()
|
||||
loadExecutions()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.resetCurrentTask()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-task-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-section,
|
||||
.executions-section {
|
||||
margin-top: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
449
Moncter/TaskShow/src/views/TagTask/TaskForm.vue
Normal file
449
Moncter/TaskShow/src/views/TagTask/TaskForm.vue
Normal file
@@ -0,0 +1,449 @@
|
||||
<template>
|
||||
<div class="tag-task-form">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ isEdit ? '编辑标签任务' : '创建标签任务' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider>基本信息</el-divider>
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入任务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入任务描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<el-radio-group v-model="form.task_type" @change="handleTaskTypeChange">
|
||||
<el-radio label="full">全量计算</el-radio>
|
||||
<el-radio label="incremental">增量计算</el-radio>
|
||||
<el-radio label="specific">指定用户</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">
|
||||
<span v-if="form.task_type === 'full'">全量计算:计算所有用户的标签</span>
|
||||
<span v-else-if="form.task_type === 'incremental'">增量计算:只计算有数据变更的用户</span>
|
||||
<span v-else-if="form.task_type === 'specific'">指定用户:只计算指定范围的用户</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标标签" prop="target_tag_ids">
|
||||
<el-select
|
||||
v-model="form.target_tag_ids"
|
||||
multiple
|
||||
placeholder="请选择要计算的标签"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="tag in tagDefinitions"
|
||||
:key="tag.tag_id"
|
||||
:label="tag.tag_name"
|
||||
:value="tag.tag_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 用户范围配置 -->
|
||||
<el-divider>用户范围配置</el-divider>
|
||||
<el-form-item label="用户范围" prop="user_scope.type">
|
||||
<el-radio-group v-model="form.user_scope.type" @change="handleUserScopeTypeChange">
|
||||
<el-radio label="all">全部用户</el-radio>
|
||||
<el-radio label="list">指定用户列表</el-radio>
|
||||
<el-radio label="filter">按条件筛选</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 指定用户列表 -->
|
||||
<el-form-item
|
||||
v-if="form.user_scope.type === 'list'"
|
||||
label="用户ID列表"
|
||||
prop="user_scope.user_ids"
|
||||
>
|
||||
<el-input
|
||||
v-model="userIdsText"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入用户ID,每行一个"
|
||||
@blur="handleUserIdsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 按条件筛选 -->
|
||||
<el-form-item
|
||||
v-if="form.user_scope.type === 'filter'"
|
||||
label="筛选条件"
|
||||
>
|
||||
<el-button type="primary" @click="handleAddFilterCondition">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加条件
|
||||
</el-button>
|
||||
<el-table
|
||||
:data="form.user_scope.filter_conditions"
|
||||
border
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
<el-table-column label="字段" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.field" placeholder="字段名" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运算符" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.operator">
|
||||
<el-option label="大于" value="gt" />
|
||||
<el-option label="大于等于" value="gte" />
|
||||
<el-option label="小于" value="lt" />
|
||||
<el-option label="小于等于" value="lte" />
|
||||
<el-option label="等于" value="eq" />
|
||||
<el-option label="不等于" value="ne" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="值">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.value" placeholder="值" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleRemoveFilterCondition($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 调度配置 -->
|
||||
<el-divider>调度配置</el-divider>
|
||||
<el-form-item label="启用调度">
|
||||
<el-switch v-model="form.schedule.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.schedule.enabled"
|
||||
label="Cron表达式"
|
||||
prop="schedule.cron"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.schedule.cron"
|
||||
placeholder="例如: 0 2 * * * (每天凌晨2点)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 高级配置 -->
|
||||
<el-divider>高级配置</el-divider>
|
||||
<el-form-item label="并发数">
|
||||
<el-input-number
|
||||
v-model="form.config.concurrency"
|
||||
:min="1"
|
||||
:max="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="批量大小">
|
||||
<el-input-number
|
||||
v-model="form.config.batch_size"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="错误处理">
|
||||
<el-select v-model="form.config.error_handling">
|
||||
<el-option label="跳过错误继续" value="skip" />
|
||||
<el-option label="遇到错误停止" value="stop" />
|
||||
<el-option label="重试" value="retry" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useTagTaskStore } from '@/store'
|
||||
import { useTagDefinitionStore } from '@/store'
|
||||
import type { TagTask, TagDefinition } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const tagTaskStore = useTagTaskStore()
|
||||
const tagDefinitionStore = useTagDefinitionStore()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
const tagDefinitions = ref<TagDefinition[]>([])
|
||||
const userIdsText = ref('')
|
||||
|
||||
const form = reactive<Partial<TagTask>>({
|
||||
name: '',
|
||||
description: '',
|
||||
task_type: 'full',
|
||||
target_tag_ids: [],
|
||||
user_scope: {
|
||||
type: 'all',
|
||||
user_ids: [],
|
||||
filter_conditions: []
|
||||
},
|
||||
schedule: {
|
||||
enabled: false,
|
||||
cron: ''
|
||||
},
|
||||
config: {
|
||||
concurrency: 10,
|
||||
batch_size: 100,
|
||||
error_handling: 'skip'
|
||||
}
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
'target_tag_ids': [{ required: true, message: '请选择目标标签', trigger: 'change' }],
|
||||
'user_scope.user_ids': [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (form.user_scope?.type === 'list') {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请至少输入一个用户ID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'user_scope.filter_conditions': [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (form.user_scope?.type === 'filter') {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请至少添加一个筛选条件'))
|
||||
} else {
|
||||
// 验证每个条件是否完整
|
||||
const incomplete = value.some((cond: any) => !cond.field || !cond.operator || cond.value === '')
|
||||
if (incomplete) {
|
||||
callback(new Error('请完善所有筛选条件'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
'schedule.cron': [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (form.schedule?.enabled) {
|
||||
if (!value || value.trim() === '') {
|
||||
callback(new Error('启用调度时必须填写Cron表达式'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const loadTagDefinitions = async () => {
|
||||
try {
|
||||
await tagDefinitionStore.getActiveDefinitions()
|
||||
tagDefinitions.value = tagDefinitionStore.definitions
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载标签定义失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserIdsChange = () => {
|
||||
if (userIdsText.value) {
|
||||
form.user_scope!.user_ids = userIdsText.value
|
||||
.split('\n')
|
||||
.map(id => id.trim())
|
||||
.filter(id => id)
|
||||
} else {
|
||||
form.user_scope!.user_ids = []
|
||||
}
|
||||
}
|
||||
|
||||
// 在提交前同步用户ID列表(防止用户没有触发blur事件)
|
||||
const syncUserIdsBeforeSubmit = () => {
|
||||
if (form.user_scope?.type === 'list') {
|
||||
handleUserIdsChange()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddFilterCondition = () => {
|
||||
form.user_scope!.filter_conditions = form.user_scope!.filter_conditions || []
|
||||
form.user_scope!.filter_conditions.push({
|
||||
field: '',
|
||||
operator: 'eq',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveFilterCondition = (index: number) => {
|
||||
form.user_scope!.filter_conditions?.splice(index, 1)
|
||||
}
|
||||
|
||||
// 处理任务类型变化
|
||||
const handleTaskTypeChange = () => {
|
||||
// 当任务类型为 specific(指定用户)时,如果用户范围是 all,自动切换为 list
|
||||
if (form.task_type === 'specific' && form.user_scope?.type === 'all') {
|
||||
form.user_scope.type = 'list'
|
||||
}
|
||||
// 当任务类型为 full(全量计算)时,自动切换用户范围为 all
|
||||
else if (form.task_type === 'full' && form.user_scope?.type !== 'all') {
|
||||
form.user_scope!.type = 'all'
|
||||
// 清理不需要的数据
|
||||
form.user_scope!.user_ids = []
|
||||
form.user_scope!.filter_conditions = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户范围类型变化
|
||||
const handleUserScopeTypeChange = () => {
|
||||
if (!form.user_scope) return
|
||||
|
||||
// 清理不需要的数据
|
||||
if (form.user_scope.type === 'all') {
|
||||
form.user_scope.user_ids = []
|
||||
form.user_scope.filter_conditions = []
|
||||
userIdsText.value = ''
|
||||
} else if (form.user_scope.type === 'list') {
|
||||
form.user_scope.filter_conditions = []
|
||||
} else if (form.user_scope.type === 'filter') {
|
||||
form.user_scope.user_ids = []
|
||||
userIdsText.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 提交前同步用户ID列表
|
||||
syncUserIdsBeforeSubmit()
|
||||
|
||||
// 清理不需要的数据
|
||||
if (form.user_scope) {
|
||||
if (form.user_scope.type === 'all') {
|
||||
// 全量计算时,清除用户ID列表和筛选条件
|
||||
form.user_scope.user_ids = []
|
||||
form.user_scope.filter_conditions = []
|
||||
} else if (form.user_scope.type === 'list') {
|
||||
// 指定用户列表时,清除筛选条件
|
||||
form.user_scope.filter_conditions = []
|
||||
} else if (form.user_scope.type === 'filter') {
|
||||
// 按条件筛选时,清除用户ID列表
|
||||
form.user_scope.user_ids = []
|
||||
}
|
||||
}
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await tagTaskStore.updateTask(route.params.id as string, form)
|
||||
ElMessage.success('任务更新成功')
|
||||
} else {
|
||||
await tagTaskStore.createTask(form)
|
||||
ElMessage.success('任务创建成功')
|
||||
}
|
||||
router.push('/tag-tasks')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadTaskDetail = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const task = await tagTaskStore.fetchTaskDetail(route.params.id as string)
|
||||
if (task) {
|
||||
Object.assign(form, {
|
||||
name: task.name,
|
||||
description: task.description,
|
||||
task_type: task.task_type,
|
||||
target_tag_ids: task.target_tag_ids || [],
|
||||
user_scope: task.user_scope || { type: 'all' },
|
||||
schedule: task.schedule || { enabled: false, cron: '' },
|
||||
config: task.config || {
|
||||
concurrency: 10,
|
||||
batch_size: 100,
|
||||
error_handling: 'skip'
|
||||
}
|
||||
})
|
||||
|
||||
if (task.user_scope?.user_ids && task.user_scope.user_ids.length > 0) {
|
||||
userIdsText.value = task.user_scope.user_ids.join('\n')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTagDefinitions()
|
||||
if (isEdit.value) {
|
||||
loadTaskDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-task-form {
|
||||
.card-header {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
310
Moncter/TaskShow/src/views/TagTask/TaskList.vue
Normal file
310
Moncter/TaskShow/src/views/TagTask/TaskList.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="tag-task-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>标签任务列表</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建任务
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="任务名称">
|
||||
<el-input
|
||||
v-model="filters.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select
|
||||
v-model="filters.task_type"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="全量计算" value="full" />
|
||||
<el-option label="增量计算" value="incremental" />
|
||||
<el-option label="指定用户" value="specific" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="待启动" value="pending" />
|
||||
<el-option label="运行中" value="running" />
|
||||
<el-option label="已暂停" value="paused" />
|
||||
<el-option label="已停止" value="stopped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="错误" value="error" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table :data="taskList" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="name" label="任务名称" min-width="200" />
|
||||
<el-table-column prop="task_type" label="任务类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.task_type === 'full'" type="primary">全量</el-tag>
|
||||
<el-tag v-else-if="row.task_type === 'incremental'" type="success">增量</el-tag>
|
||||
<el-tag v-else type="info">指定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_tag_ids" label="目标标签" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="tagId in row.target_tag_ids?.slice(0, 2)" :key="tagId" size="small" style="margin-right: 5px;">
|
||||
{{ tagId }}
|
||||
</el-tag>
|
||||
<span v-if="row.target_tag_ids?.length > 2">...</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<StatusBadge :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress.percentage" label="进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.progress?.percentage || 0"
|
||||
:status="row.progress?.status === 'error' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'pending' || row.status === 'paused'"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleStart(row)"
|
||||
>
|
||||
启动
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'running'"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handlePause(row)"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'running' || row.status === 'paused'"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleStop(row)"
|
||||
>
|
||||
停止
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
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">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import StatusBadge from '@/components/StatusBadge/index.vue'
|
||||
import { useTagTaskStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import type { TagTask } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useTagTaskStore()
|
||||
|
||||
const filters = ref({
|
||||
name: '',
|
||||
task_type: '',
|
||||
status: ''
|
||||
})
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const taskList = computed(() => store.tasks)
|
||||
const loading = computed(() => store.loading)
|
||||
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const result = await store.fetchTasks({
|
||||
...filters.value,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
pagination.value.total = result.total
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '加载任务列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.page = 1
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
name: '',
|
||||
task_type: '',
|
||||
status: ''
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push('/tag-tasks/create')
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
router.push(`/tag-tasks/${row.task_id}`)
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
router.push(`/tag-tasks/${row.task_id}/edit`)
|
||||
}
|
||||
|
||||
const handleStart = async (row: TagTask) => {
|
||||
try {
|
||||
await store.startTask(row.task_id)
|
||||
ElMessage.success('任务已启动')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '启动任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = async (row: TagTask) => {
|
||||
try {
|
||||
await store.pauseTask(row.task_id)
|
||||
ElMessage.success('任务已暂停')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '暂停任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async (row: TagTask) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要停止该任务吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.stopTask(row.task_id)
|
||||
ElMessage.success('任务已停止')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '停止任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: TagTask) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该任务吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await store.deleteTask(row.task_id)
|
||||
ElMessage.success('任务已删除')
|
||||
await loadTasks()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-task-list {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
16
Moncter/TaskShow/src/vite-env.d.ts
vendored
Normal file
16
Moncter/TaskShow/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Reference in New Issue
Block a user