🔨 tag 管理.

This commit is contained in:
lijiahangmax
2025-10-29 10:39:19 +08:00
parent 6a13d3cb22
commit 5d86c330fe
12 changed files with 591 additions and 28 deletions

View File

@@ -33,6 +33,11 @@
"type": "java.lang.Boolean",
"description": "是否开启 cors 过滤器."
},
{
"name": "orion.api.ip-headers",
"type": "java.lang.String",
"description": "获取 IP 的请求头."
},
{
"name": "orion.api.expose.header",
"type": "java.lang.String",

View File

@@ -20,43 +20,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.entity.dto;
package org.dromara.visor.module.infra.entity.po;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.visor.common.entity.RequestIdentity;
import java.io.Serializable;
/**
* 身份信息
* 标签关联数量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/11/1 1:01
* @since 2024/12/23 16:24
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginTokenIdentityDTO implements RequestIdentity {
@Schema(name = "TagRelCountPO", description = "标签关联数量")
public class TagRelCountPO implements Serializable {
/**
* 原始登录时间
*/
private Long loginTime;
private static final long serialVersionUID = 1L;
/**
* 当前设备登录地址
*/
private String address;
@Schema(description = "tagId")
private Long tagId;
/**
* 当前设备登录地址
*/
private String location;
/**
* 当前设备 userAgent
*/
private String userAgent;
@Schema(description = "数量")
private Integer count;
}

View File

@@ -1,19 +1,47 @@
import type { TableData } from '@arco-design/web-vue';
import type { DataGrid, Pagination, OrderDirection } from '@/types/global';
import axios from 'axios';
export type TagType = 'HOST' | string
export type TagType = 'HOST' | 'MONITOR_DASH' | string
/**
* tag 创建对象
*/
export interface TagCreateRequest {
name: number;
type: TagType;
name?: string;
type?: TagType;
}
/**
* tag 修改对象
*/
export interface TagUpdateRequest extends TagCreateRequest {
id?: number;
}
/**
* tag 查询请求
*/
export interface TagQueryRequest extends Pagination, OrderDirection {
name?: string;
type?: string;
}
/**
* tag 响应对象
*/
export interface TagQueryResponse {
export interface TagQueryResponse extends TableData, TagItem {
relCount: string;
createTime: number;
updateTime: number;
creator: string;
updater: string;
}
/**
* tag 元素
*/
export interface TagItem {
id: number;
name: string;
}
@@ -25,9 +53,30 @@ export function createTag(request: TagCreateRequest) {
return axios.post('/infra/tag/create', request);
}
/**
* 修改标签
*/
export function updateTag(request: TagUpdateRequest) {
return axios.put('/infra/tag/update', request);
}
/**
* 分页查询标签
*/
export function getTagPage(request: TagQueryRequest) {
return axios.post<DataGrid<TagQueryResponse>>('/infra/tag/query', request);
}
/**
* 查询标签
*/
export function getTagList(type: TagType) {
return axios.get<Array<TagQueryResponse>>('/infra/tag/list', { params: { type } });
return axios.get<Array<TagItem>>('/infra/tag/list', { params: { type } });
}
/**
* 通过 id 删除标签
*/
export function deleteTag(id: number) {
return axios.delete('/infra/tag/delete', { params: { id } });
}

View File

@@ -26,6 +26,11 @@ const SYSTEM: AppRouteRecordRaw = {
path: '/system/notify-template',
component: () => import('@/views/system/notify-template/index.vue'),
},
{
name: 'systemTags',
path: '/system/tags',
component: () => import('@/views/system/tags/index.vue'),
},
{
name: 'systemSetting',
path: '/system/setting',

View File

@@ -12,3 +12,18 @@ export const EnabledStatus = {
DISABLED: 0,
ENABLED: 1
};
// 开关值
export const SwitchValue = {
OFF: 0,
ON: 1
};
// tag 颜色
export const TagColors = [
'arcoblue',
'green',
'purple',
'pinkpurple',
'magenta'
];

View File

@@ -41,7 +41,8 @@
<a-tabs v-model:active-key="bizType"
direction="vertical"
type="rounded"
:hide-content="true">
:hide-content="true"
@change="reload">
<a-tab-pane v-for="item in toOptions(BizTypeKey)"
:key="item.value as string"
:title="item.label" />

View File

@@ -0,0 +1,129 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-large"
title-align="start"
:title="title"
:top="80"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handleOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 标签名称 -->
<a-form-item field="name" label="标签名称">
<a-input v-model="formModel.name"
placeholder="请输入标签名称"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'systemTagFormModal'
};
</script>
<script lang="ts" setup>
import type { TagUpdateRequest } from '@/api/meta/tag';
import type { FormHandle } from '@/types/form';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { createTag, updateTag } from '@/api/meta/tag';
import { Message } from '@arco-design/web-vue';
import { assignOmitRecord } from '@/utils';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const formHandle = ref<FormHandle>('add');
const defaultForm = (): TagUpdateRequest => {
return {
id: undefined,
name: undefined,
};
};
const formRef = ref();
const formModel = ref<TagUpdateRequest>({});
const emits = defineEmits(['added', 'updated']);
// 打开新增
const openAdd = (_type: string) => {
title.value = '添加标签';
formHandle.value = 'add';
formModel.value = assignOmitRecord({ ...defaultForm(), type: _type });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改标签';
formHandle.value = 'update';
formModel.value = assignOmitRecord({ ...defaultForm(), ...record });
setVisible(true);
};
defineExpose({ openAdd, openUpdate });
// 确定
const handleOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
if (formHandle.value === 'add') {
// 新增
await createTag(formModel.value);
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateTag(formModel.value);
Message.success('修改成功');
emits('updated');
}
handleClose();
return true;
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handleClear();
setVisible(false);
};
// 清空
const handleClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,231 @@
<template>
<!-- 内容部分 -->
<div class="container-content">
<!-- 业务类型 -->
<a-card class="general-card table-search-card biz-card">
<a-tabs v-model:active-key="tagType"
direction="vertical"
type="rounded"
:hide-content="true"
@change="reload">
<a-tab-pane v-for="item in toOptions(TagTypeKey)"
:key="item.value as string"
:title="item.label" />
</a-tabs>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
标签列表
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 标签名称 -->
<a-input v-model="formModel.name"
style="width: 168px;"
placeholder="标签名称"
allow-clear
@press-enter="reload" />
<!-- 重置 -->
<a-button v-permission="['infra:tag:create']"
@click="resetQuery">
重置
<template #icon>
<icon-refresh />
</template>
</a-button>
<!-- 查询 -->
<a-button v-permission="['infra:tag:create']"
type="primary"
@click="() => fetchTableData()">
查询
<template #icon>
<icon-search />
</template>
</a-button>
<a-divider direction="vertical"
style="height: 22px; margin: 0 8px"
:size="2" />
<!-- 新增 -->
<a-button v-permission="['infra:tag:create']"
type="primary"
@click="emits('openAdd', tagType)">
<template #icon>
<icon-plus />
</template>
新增
</a-button>
<!-- 调整 -->
<table-adjust :columns="columns"
:columns-hook="columnsHook"
:query-order="queryOrder"
@query="fetchTableData" />
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
ref="tableRef"
class="table-resize"
:loading="loading"
:columns="tableColumns"
:data="tableRenderData"
:pagination="pagination"
:bordered="false"
:column-resizable="true"
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size: number) => fetchTableData(1, size)">
<!-- 标签名称 -->
<template #name="{ record }">
<a-tag :color="dataColor(record.name, TagColors)">
{{ record.name }}
</a-tag>
</template>
<!-- 关联数量 -->
<template #relCount="{ record }">
<span class="span-blue">
{{ record.relCount }} 个
</span>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改 -->
<a-button v-permission="['infra:tag:update']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这个标签以及关联关系吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['infra:tag:delete']"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts">
export default {
name: 'systemTagTable'
};
</script>
<script lang="ts" setup>
import type { TagQueryRequest, TagQueryResponse } from '@/api/meta/tag';
import { reactive, ref, onMounted } from 'vue';
import { deleteTag, getTagPage } from '@/api/meta/tag';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { TableName, TagTypeKey } from '../types/const';
import { useTablePagination, useTableColumns } from '@/hooks/table';
import { useDictStore } from '@/store';
import { useQueryOrder, ASC } from '@/hooks/query-order';
import { dataColor } from '@/utils';
import { TagColors } from '@/types/const';
import TableAdjust from '@/components/app/table-adjust/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate']);
const pagination = useTablePagination();
const { loading, setLoading } = useLoading();
const queryOrder = useQueryOrder(TableName, ASC);
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
const { toOptions } = useDictStore();
const tagType = ref('HOST');
const tableRenderData = ref<Array<TagQueryResponse>>([]);
const formModel = reactive<TagQueryRequest>({
name: undefined,
});
// 删除当前行
const deleteRow = async (record: TagQueryResponse) => {
try {
setLoading(true);
// 调用删除接口
await deleteTag(record.id);
Message.success('删除成功');
// 重新加载
reload();
} catch (e) {
} finally {
setLoading(false);
}
};
// 重新加载
const reload = () => {
// 重新加载数据
fetchTableData();
};
defineExpose({ reload });
// 重置查询
const resetQuery = () => {
formModel.name = undefined;
fetchTableData();
};
// 加载数据
const doFetchTableData = async (request: TagQueryRequest) => {
try {
setLoading(true);
const { data } = await getTagPage(queryOrder.markOrderly({ ...request, type: tagType.value }));
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
fetchTableData();
});
</script>
<style lang="less" scoped>
@biz-card-width: 148px;
.container-content {
display: flex;
}
.biz-card {
width: @biz-card-width;
margin: 0 16px 0 0 !important;
user-select: none;
}
.table-card {
width: calc(100% - @biz-card-width - 16px);
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<tag-table ref="table"
@open-add="(e: any) => modal.openAdd(e)"
@open-update="(e: any) => modal.openUpdate(e)" />
<!-- 添加修改抽屉 -->
<tag-form-modal ref="modal"
@added="reload"
@updated="reload" />
</div>
</template>
<script lang="ts">
export default {
name: 'systemTags'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue';
import { useDictStore } from '@/store';
import { dictKeys } from './types/const';
import TagTable from './components/tag-table.vue';
import TagFormModal from './components/tag-form-modal.vue';
const render = ref(false);
const table = ref();
const modal = ref();
// 重新加载
const reload = () => {
table.value.reload();
};
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
render.value = true;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,7 @@
export const TableName = 'tag_list';
// 标签类型 字典项
export const TagTypeKey = 'tagType';
// 加载的字典值
export const dictKeys = [TagTypeKey];

View File

@@ -0,0 +1,11 @@
import type { FieldRule } from '@arco-design/web-vue';
export default {
name: [{
required: true,
message: '请输入标签名称'
}, {
maxLength: 32,
message: '标签名称长度不能大于32位'
}],
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -0,0 +1,71 @@
import type { TableColumnData } from '@arco-design/web-vue';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 68,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '标签名称',
dataIndex: 'name',
slotName: 'name',
align: 'left',
minWidth: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '关联数量',
dataIndex: 'relCount',
slotName: 'relCount',
align: 'left',
minWidth: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
default: true,
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
}, {
title: '创建人',
width: 148,
dataIndex: 'creator',
slotName: 'creator',
default: true,
}, {
title: '修改人',
width: 148,
dataIndex: 'updater',
slotName: 'updater',
}, {
title: '操作',
slotName: 'handle',
width: 138,
align: 'center',
fixed: 'right',
default: true,
},
] as TableColumnData[];
export default columns;