feat
This commit is contained in:
93
src/api/kb/category.ts
Normal file
93
src/api/kb/category.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 分类类型 */
|
||||
export interface Category {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
parent_id: number;
|
||||
level: number;
|
||||
path: string;
|
||||
sort_order: number;
|
||||
status: string;
|
||||
creator_id: number;
|
||||
creator_name: string;
|
||||
doc_count: number;
|
||||
faq_count: number;
|
||||
metadata: string | null;
|
||||
remarks: string;
|
||||
}
|
||||
|
||||
/** API响应包装类型 */
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 创建分类请求参数 */
|
||||
export interface CreateCategoryParams {
|
||||
name: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
parent_id?: number;
|
||||
sort_order?: number;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
/** 更新分类请求参数 */
|
||||
export interface UpdateCategoryParams {
|
||||
id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
sort_order?: number;
|
||||
status?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
/** 获取分类列表参数 */
|
||||
export interface FetchCategoryListParams {
|
||||
type?: string;
|
||||
parent_id?: number;
|
||||
}
|
||||
|
||||
/** 创建分类 */
|
||||
export const createCategory = (data: CreateCategoryParams) => {
|
||||
return request.post<ApiResponse<Category>>("/Kb/v1/category/create", data);
|
||||
};
|
||||
|
||||
/** 更新分类 */
|
||||
export const updateCategory = (data: UpdateCategoryParams) => {
|
||||
return request.post<ApiResponse<Category>>("/Kb/v1/category/update", data);
|
||||
};
|
||||
|
||||
/** 删除分类 */
|
||||
export const deleteCategory = (id: number) => {
|
||||
return request.delete<ApiResponse<string>>(`/Kb/v1/category/${id}`);
|
||||
};
|
||||
|
||||
/** 获取分类详情 */
|
||||
export const fetchCategoryDetail = (id: number) => {
|
||||
return request.get<ApiResponse<Category>>(`/Kb/v1/category/${id}`);
|
||||
};
|
||||
|
||||
/** 获取分类列表 */
|
||||
export const fetchCategoryList = (params?: FetchCategoryListParams) => {
|
||||
return request.get<ApiResponse<Category[]>>("/Kb/v1/category/list", { params });
|
||||
};
|
||||
|
||||
/** 获取分类树 */
|
||||
export const fetchCategoryTree = (type?: string) => {
|
||||
return request.get<ApiResponse<Category[]>>("/Kb/v1/category/tree", {
|
||||
params: type ? { type } : undefined,
|
||||
});
|
||||
};
|
||||
212
src/api/kb/document.ts
Normal file
212
src/api/kb/document.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 文档状态 */
|
||||
export type DocumentStatus = 'draft' | 'published' | 'reviewed' | 'rejected';
|
||||
|
||||
/** 文档类型 */
|
||||
export type DocumentType = 'common' | 'guide' | 'solution' | 'troubleshoot' | 'process' | 'technical';
|
||||
|
||||
/** 文档接口类型 */
|
||||
export interface Document {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
doc_no: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
type: DocumentType;
|
||||
status: DocumentStatus;
|
||||
category_id: number;
|
||||
sub_category: string;
|
||||
author_id: number;
|
||||
author_name: string;
|
||||
reviewer_id: number;
|
||||
reviewer_name: string;
|
||||
reviewed_at: string | null;
|
||||
published_at: string | null;
|
||||
publisher_id: number;
|
||||
view_count: number;
|
||||
like_count: number;
|
||||
comment_count: number;
|
||||
download_count: number;
|
||||
version: string;
|
||||
version_notes: string;
|
||||
tags: string;
|
||||
attachments: string | null;
|
||||
related_docs: string | null;
|
||||
metadata: string | null;
|
||||
keywords: string;
|
||||
remarks: string;
|
||||
is_favorited?: boolean;
|
||||
}
|
||||
|
||||
/** API响应包装类型 */
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 分页响应类型 */
|
||||
export interface PaginatedResponse<T> {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
data: T[];
|
||||
}
|
||||
|
||||
/** 创建文档请求参数 */
|
||||
export interface CreateDocumentParams {
|
||||
title: string;
|
||||
description?: string;
|
||||
content: string;
|
||||
type?: DocumentType;
|
||||
category_id?: number;
|
||||
sub_category?: string;
|
||||
keywords?: string;
|
||||
tags?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
/** 更新文档请求参数 */
|
||||
export interface UpdateDocumentParams {
|
||||
id: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
type?: DocumentType;
|
||||
category_id?: number;
|
||||
sub_category?: string;
|
||||
keywords?: string;
|
||||
tags?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
/** 获取文档列表参数 */
|
||||
export interface FetchDocumentListParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
type?: DocumentType;
|
||||
status?: DocumentStatus;
|
||||
category_id?: number;
|
||||
}
|
||||
|
||||
/** 文档类型选项 */
|
||||
export const documentTypeOptions = [
|
||||
{ label: '通用文档', value: 'common' },
|
||||
{ label: '操作指南', value: 'guide' },
|
||||
{ label: '解决方案', value: 'solution' },
|
||||
{ label: '故障排查', value: 'troubleshoot' },
|
||||
{ label: '流程规范', value: 'process' },
|
||||
{ label: '技术文档', value: 'technical' },
|
||||
];
|
||||
|
||||
/** 文档状态选项 */
|
||||
export const documentStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已发布', value: 'published' },
|
||||
{ label: '已审核', value: 'reviewed' },
|
||||
{ label: '未通过审核', value: 'rejected' },
|
||||
];
|
||||
|
||||
/** 获取文档状态文本 */
|
||||
export const getDocumentStatusText = (status: DocumentStatus): string => {
|
||||
const statusMap: Record<DocumentStatus, string> = {
|
||||
draft: '草稿',
|
||||
published: '已发布',
|
||||
reviewed: '已审核',
|
||||
rejected: '未通过审核',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
/** 获取文档状态颜色 */
|
||||
export const getDocumentStatusColor = (status: DocumentStatus): string => {
|
||||
const colorMap: Record<DocumentStatus, string> = {
|
||||
draft: 'gray',
|
||||
published: 'blue',
|
||||
reviewed: 'green',
|
||||
rejected: 'red',
|
||||
};
|
||||
return colorMap[status] || 'gray';
|
||||
};
|
||||
|
||||
/** 获取文档类型文本 */
|
||||
export const getDocumentTypeText = (type: DocumentType): string => {
|
||||
const typeMap: Record<DocumentType, string> = {
|
||||
common: '通用文档',
|
||||
guide: '操作指南',
|
||||
solution: '解决方案',
|
||||
troubleshoot: '故障排查',
|
||||
process: '流程规范',
|
||||
technical: '技术文档',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
/** 创建文档 */
|
||||
export const createDocument = (data: CreateDocumentParams) => {
|
||||
return request.post<ApiResponse<Document>>("/Kb/v1/document/create", data);
|
||||
};
|
||||
|
||||
/** 更新文档 */
|
||||
export const updateDocument = (data: UpdateDocumentParams) => {
|
||||
return request.post<ApiResponse<Document>>("/Kb/v1/document/update", data);
|
||||
};
|
||||
|
||||
/** 删除文档(移入回收站) */
|
||||
export const deleteDocument = (id: number) => {
|
||||
return request.delete<ApiResponse<string>>(`/Kb/v1/document/${id}`);
|
||||
};
|
||||
|
||||
/** 获取文档详情 */
|
||||
export const fetchDocumentDetail = (id: number) => {
|
||||
return request.get<ApiResponse<Document>>(`/Kb/v1/document/${id}`);
|
||||
};
|
||||
|
||||
/** 获取文档列表 */
|
||||
export const fetchDocumentList = (params?: FetchDocumentListParams) => {
|
||||
return request.get<ApiResponse<PaginatedResponse<Document>>>("/Kb/v1/document/list", { params });
|
||||
};
|
||||
|
||||
/** 发布文档 */
|
||||
export const publishDocument = (id: number) => {
|
||||
return request.post<ApiResponse<string>>("/Kb/v1/document/publish", { id });
|
||||
};
|
||||
|
||||
/** 移入回收站 */
|
||||
export const moveToTrash = (id: number) => {
|
||||
return request.post<ApiResponse<string>>("/Kb/v1/trash/move", { id, type: 'document' });
|
||||
};
|
||||
|
||||
/** 获取我的文档列表(由我创建的所有文档) */
|
||||
export const fetchMyDocumentList = (params?: FetchDocumentListParams) => {
|
||||
return request.get<ApiResponse<PaginatedResponse<Document>>>("/Kb/v1/review/publish/list", { params });
|
||||
};
|
||||
|
||||
/** 获取已审核通过的文档列表 */
|
||||
export const fetchApprovedDocumentList = (params?: FetchDocumentListParams) => {
|
||||
return request.get<ApiResponse<PaginatedResponse<Document>>>("/Kb/v1/review/approved/list", { params });
|
||||
};
|
||||
|
||||
/** 收藏文档 */
|
||||
export const favoriteDocument = (id: number) => {
|
||||
return request.post<ApiResponse<string>>("/Kb/v1/favorite/create", { resource_type: 'document', resource_id: id });
|
||||
};
|
||||
|
||||
/** 取消收藏文档 */
|
||||
export const unfavoriteDocument = (id: number) => {
|
||||
return request.delete<ApiResponse<string>>("/Kb/v1/favorite/delete", { data: { resource_type: 'document', resource_id: id } });
|
||||
};
|
||||
|
||||
/** 检查文档是否已收藏 */
|
||||
export const checkFavorite = (id: number) => {
|
||||
return request.get<ApiResponse<{ is_favorited: boolean }>>(`/Kb/v1/favorite/check`, { params: { resource_type: 'document', resource_id: id } });
|
||||
};
|
||||
|
||||
/** 下载文档 */
|
||||
export const downloadDocument = (id: number) => {
|
||||
return request.get<Blob>(`/Kb/v1/document/${id}/download`, { responseType: 'blob' });
|
||||
};
|
||||
88
src/api/kb/trash.ts
Normal file
88
src/api/kb/trash.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** API响应包装类型 */
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 分页响应类型 */
|
||||
export interface PaginatedResponse<T> {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
data: T[];
|
||||
}
|
||||
|
||||
/** 回收站记录 */
|
||||
export interface TrashRecord {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
resource_type: 'document' | 'faq';
|
||||
resource_id: number;
|
||||
resource_name: string;
|
||||
deleted_by: number;
|
||||
deleted_name: string;
|
||||
deleted_time: string;
|
||||
delete_reason: string;
|
||||
original_data: string;
|
||||
remarks: string;
|
||||
}
|
||||
|
||||
/** 获取回收站列表参数 */
|
||||
export interface FetchTrashListParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
resource_type?: 'document' | 'faq';
|
||||
}
|
||||
|
||||
/** 恢复资源请求参数 */
|
||||
export interface RestoreTrashParams {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/** 彻底删除请求参数 */
|
||||
export interface DeleteTrashParams {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/** 资源类型选项 */
|
||||
export const resourceTypeOptions = [
|
||||
{ label: '文档', value: 'document' },
|
||||
{ label: '常见问题', value: 'faq' },
|
||||
];
|
||||
|
||||
/** 获取资源类型文本 */
|
||||
export const getResourceTypeText = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
document: '文档',
|
||||
faq: '常见问题',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
/** 获取资源类型颜色 */
|
||||
export const getResourceTypeColor = (type: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
document: 'blue',
|
||||
faq: 'green',
|
||||
};
|
||||
return colorMap[type] || 'gray';
|
||||
};
|
||||
|
||||
/** 获取回收站列表 */
|
||||
export const fetchTrashList = (params?: FetchTrashListParams) => {
|
||||
return request.get<ApiResponse<PaginatedResponse<TrashRecord>>>("/Kb/v1/trash/list", { params });
|
||||
};
|
||||
|
||||
/** 恢复资源 */
|
||||
export const restoreTrash = (data: RestoreTrashParams) => {
|
||||
return request.post<ApiResponse<string>>("/Kb/v1/trash/restore", data);
|
||||
};
|
||||
|
||||
/** 彻底删除 */
|
||||
export const deleteTrash = (data: DeleteTrashParams) => {
|
||||
return request.post<ApiResponse<string>>("/Kb/v1/trash/delete", data);
|
||||
};
|
||||
139
src/api/ops/asset.ts
Normal file
139
src/api/ops/asset.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 资产状态枚举 */
|
||||
export enum AssetStatus {
|
||||
IN_USE = 'in_use', // 在用
|
||||
IDLE = 'idle', // 闲置
|
||||
MAINTAIN = 'maintain', // 维修中
|
||||
SCRAP = 'scrap', // 待报废
|
||||
DISPOSED = 'disposed', // 已报废
|
||||
}
|
||||
|
||||
/** 资产状态选项 */
|
||||
export const assetStatusOptions = [
|
||||
{ label: '在用', value: AssetStatus.IN_USE },
|
||||
{ label: '闲置', value: AssetStatus.IDLE },
|
||||
{ label: '维修中', value: AssetStatus.MAINTAIN },
|
||||
{ label: '待报废', value: AssetStatus.SCRAP },
|
||||
{ label: '已报废', value: AssetStatus.DISPOSED },
|
||||
];
|
||||
|
||||
/** 获取资产状态文本 */
|
||||
export const getAssetStatusText = (status: string) => {
|
||||
const item = assetStatusOptions.find(opt => opt.value === status);
|
||||
return item?.label || status;
|
||||
};
|
||||
|
||||
/** 获取资产状态颜色 */
|
||||
export const getAssetStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
[AssetStatus.IN_USE]: 'green',
|
||||
[AssetStatus.IDLE]: 'blue',
|
||||
[AssetStatus.MAINTAIN]: 'orange',
|
||||
[AssetStatus.SCRAP]: 'red',
|
||||
[AssetStatus.DISPOSED]: 'gray',
|
||||
};
|
||||
return colorMap[status] || 'gray';
|
||||
};
|
||||
|
||||
/** 资产列表查询参数 */
|
||||
export interface AssetListParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
category_id?: number;
|
||||
supplier_id?: number;
|
||||
datacenter_id?: number;
|
||||
department?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
/** 资产表单数据 */
|
||||
export interface AssetForm {
|
||||
id?: number;
|
||||
asset_name: string;
|
||||
asset_code: string;
|
||||
category_id?: number;
|
||||
model?: string;
|
||||
manufacturer?: string;
|
||||
serial_number?: string;
|
||||
purchase_date?: string;
|
||||
original_value?: number;
|
||||
supplier_id?: number;
|
||||
warranty_period?: string;
|
||||
warranty_expiry?: string;
|
||||
department?: string;
|
||||
user?: string;
|
||||
status?: string;
|
||||
location?: string;
|
||||
datacenter_id?: number;
|
||||
floor_id?: number;
|
||||
rack_id?: number;
|
||||
unit_start?: number;
|
||||
unit_end?: number;
|
||||
qr_code?: string;
|
||||
rfid_tag?: string;
|
||||
asset_tag?: string;
|
||||
specifications?: string;
|
||||
description?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
/** 获取资产列表(分页) */
|
||||
export const fetchAssetList = (data?: AssetListParams) => {
|
||||
return request.post("/Assets/v1/asset/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取资产详情 */
|
||||
export const fetchAssetDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/asset/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建资产 */
|
||||
export const createAsset = (data: AssetForm) => {
|
||||
return request.post("/Assets/v1/asset/create", data);
|
||||
};
|
||||
|
||||
/** 更新资产 */
|
||||
export const updateAsset = (data: AssetForm) => {
|
||||
return request.put("/Assets/v1/asset/update", data);
|
||||
};
|
||||
|
||||
/** 删除资产 */
|
||||
export const deleteAsset = (id: number) => {
|
||||
return request.delete(`/Assets/v1/asset/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 导出资产 */
|
||||
export const exportAssets = (keyword?: string) => {
|
||||
const params: any = {};
|
||||
if (keyword) params.keyword = keyword;
|
||||
return request.get("/Assets/v1/asset/export", { params });
|
||||
};
|
||||
|
||||
/** 获取资产分类列表(下拉) */
|
||||
export const fetchCategoryOptions = () => {
|
||||
return request.get("/Assets/v1/category/all");
|
||||
};
|
||||
|
||||
/** 获取供应商列表(下拉) */
|
||||
export const fetchSupplierOptions = () => {
|
||||
return request.get("/Assets/v1/supplier/all");
|
||||
};
|
||||
|
||||
/** 获取数据中心列表(下拉) */
|
||||
export const fetchDatacenterOptions = () => {
|
||||
return request.get("/Assets/v1/datacenter/list");
|
||||
};
|
||||
|
||||
/** 根据数据中心获取楼层列表 */
|
||||
export const fetchFloorOptions = (datacenterId: number) => {
|
||||
return request.get(`/Assets/v1/datacenter/${datacenterId}`);
|
||||
};
|
||||
|
||||
/** 获取机柜列表(下拉) */
|
||||
export const fetchRackOptions = (params?: { datacenter_id?: number; floor_id?: number }) => {
|
||||
return request.post("/Assets/v1/rack/list", params || {});
|
||||
};
|
||||
46
src/api/ops/assetCategory.ts
Normal file
46
src/api/ops/assetCategory.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取资产分类列表(分页) */
|
||||
export const fetchCategoryList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
parent_id?: number;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/category/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取资产分类详情 */
|
||||
export const fetchCategoryDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/category/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建资产分类 */
|
||||
export const createCategory = (data: any) => {
|
||||
return request.post("/Assets/v1/category/create", data);
|
||||
};
|
||||
|
||||
/** 更新资产分类 */
|
||||
export const updateCategory = (data: any) => {
|
||||
return request.put("/Assets/v1/category/update", data);
|
||||
};
|
||||
|
||||
/** 删除资产分类 */
|
||||
export const deleteCategory = (id: number) => {
|
||||
return request.delete(`/Assets/v1/category/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取所有资产分类(用于下拉选择) */
|
||||
export const fetchAllCategories = () => {
|
||||
return request.get("/Assets/v1/category/all");
|
||||
};
|
||||
|
||||
/** 获取资产分类树形结构 */
|
||||
export const fetchCategoryTree = () => {
|
||||
return request.get("/Assets/v1/category/tree");
|
||||
};
|
||||
|
||||
/** 获取指定分类的子分类列表 */
|
||||
export const fetchCategoryChildren = (id: number) => {
|
||||
return request.get(`/Assets/v1/category/children/${id}`);
|
||||
};
|
||||
40
src/api/ops/supplier.ts
Normal file
40
src/api/ops/supplier.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取供应商列表(分页) */
|
||||
export const fetchSupplierList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/supplier/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取供应商详情 */
|
||||
export const fetchSupplierDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/supplier/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建供应商 */
|
||||
export const createSupplier = (data: any) => {
|
||||
return request.post("/Assets/v1/supplier/create", data);
|
||||
};
|
||||
|
||||
/** 更新供应商 */
|
||||
export const updateSupplier = (data: any) => {
|
||||
return request.put("/Assets/v1/supplier/update", data);
|
||||
};
|
||||
|
||||
/** 删除供应商 */
|
||||
export const deleteSupplier = (id: number) => {
|
||||
return request.delete(`/Assets/v1/supplier/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取所有供应商(用于下拉选择) */
|
||||
export const fetchAllSuppliers = (keyword?: string, status?: string) => {
|
||||
const params: any = {};
|
||||
if (keyword) params.keyword = keyword;
|
||||
if (status) params.status = status;
|
||||
return request.get("/Assets/v1/supplier/all", { params });
|
||||
};
|
||||
@@ -83,6 +83,46 @@ const OPS: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assets/device',
|
||||
name: 'AssetsDevice',
|
||||
component: () => import('@/views/ops/pages/assets/device/index.vue'),
|
||||
meta: {
|
||||
locale: '设备管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assets/supplier',
|
||||
name: 'AssetsSupplier',
|
||||
component: () => import('@/views/ops/pages/assets/supplier/index.vue'),
|
||||
meta: {
|
||||
locale: '供应商管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assets/classify',
|
||||
name: 'AssetsClassify',
|
||||
component: () => import('@/views/ops/pages/assets/classify/index.vue'),
|
||||
meta: {
|
||||
locale: '资产分类',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'kb/items',
|
||||
name: 'KbItems',
|
||||
component: () => import('@/views/ops/pages/kb/items/index.vue'),
|
||||
meta: {
|
||||
locale: '知识库文档',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="分类详情"
|
||||
width="600px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-descriptions :data="detailData" layout="vertical" bordered :column="2" v-if="category">
|
||||
<a-descriptions-item label="分类名称" :span="1">
|
||||
{{ category.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="分类编码" :span="1">
|
||||
{{ category.code }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="父级分类" :span="1">
|
||||
{{ category.parent?.name || '无(一级分类)' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="层级" :span="1">
|
||||
{{ category.level }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="排序" :span="1">
|
||||
{{ category.sort }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="是否启用" :span="1">
|
||||
<a-tag :color="category.enabled ? 'green' : 'red'">
|
||||
{{ category.enabled ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="颜色标识" :span="2">
|
||||
<div class="color-display">
|
||||
<div
|
||||
class="color-block"
|
||||
:style="{ backgroundColor: category.color || '#ccc' }"
|
||||
></div>
|
||||
<span>{{ category.color || '-' }}</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="图标路径" :span="2">
|
||||
{{ category.icon || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ category.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ category.remarks || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间" :span="1">
|
||||
{{ formatDate(category.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间" :span="1">
|
||||
{{ formatDate(category.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人" :span="1">
|
||||
{{ category.created_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人" :span="1">
|
||||
{{ category.updated_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Category {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
parent_id?: number
|
||||
parent?: any
|
||||
level?: number
|
||||
sort?: number
|
||||
enabled?: boolean
|
||||
remarks?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
category: Category | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 详情数据
|
||||
const detailData = computed(() => {
|
||||
if (!props.category) return []
|
||||
return [
|
||||
{ label: '分类名称', value: props.category.name },
|
||||
{ label: '分类编码', value: props.category.code },
|
||||
{ label: '父级分类', value: props.category.parent?.name || '无(一级分类)' },
|
||||
{ label: '层级', value: props.category.level },
|
||||
{ label: '排序', value: props.category.sort },
|
||||
{ label: '是否启用', value: props.category.enabled ? '是' : '否' },
|
||||
{ label: '颜色标识', value: props.category.color || '-' },
|
||||
{ label: '图标路径', value: props.category.icon || '-' },
|
||||
{ label: '描述', value: props.category.description || '-' },
|
||||
{ label: '备注', value: props.category.remarks || '-' },
|
||||
{ label: '创建时间', value: formatDate(props.category.created_at) },
|
||||
{ label: '更新时间', value: formatDate(props.category.updated_at) },
|
||||
{ label: '创建人', value: props.category.created_by || '-' },
|
||||
{ label: '更新人', value: props.category.updated_by || '-' },
|
||||
]
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CategoryDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-block {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑分类' : '新增分类'"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="分类名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入分类名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="分类编码"
|
||||
field="code"
|
||||
:rules="[{ required: true, message: '请输入分类编码' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入分类编码"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item
|
||||
v-if="showParentSelector !== false"
|
||||
label="父级分类"
|
||||
field="parent_id"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.parent_id"
|
||||
placeholder="请选择父级分类(不选则为一级分类)"
|
||||
allow-clear
|
||||
allow-search
|
||||
:disabled="fixedParentId !== undefined"
|
||||
:filter-option="filterParentOption"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in parentCategories"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序" field="sort">
|
||||
<a-input-number
|
||||
v-model="form.sort"
|
||||
placeholder="请输入排序"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="是否启用" field="enabled">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="颜色标识" field="color">
|
||||
<div class="color-picker-wrapper">
|
||||
<a-input
|
||||
v-model="form.color"
|
||||
placeholder="请选择颜色标识"
|
||||
readonly
|
||||
@click="showColorPicker = !showColorPicker"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: form.color || '#ccc' }"
|
||||
></div>
|
||||
</template>
|
||||
</a-input>
|
||||
<div v-if="showColorPicker" class="color-picker-dropdown">
|
||||
<div class="color-picker-header">选择颜色</div>
|
||||
<div class="color-picker-grid">
|
||||
<div
|
||||
v-for="color in presetColors"
|
||||
:key="color"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="selectColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-picker-custom">
|
||||
<span>自定义颜色:</span>
|
||||
<input
|
||||
type="color"
|
||||
v-model="form.color"
|
||||
@change="showColorPicker = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" field="remarks">
|
||||
<a-textarea
|
||||
v-model="form.remarks"
|
||||
placeholder="请输入备注"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createCategory, updateCategory } from '@/api/ops/assetCategory'
|
||||
|
||||
interface Category {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
parent_id?: number
|
||||
sort?: number
|
||||
enabled?: boolean
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
category: Category | null
|
||||
parentCategories: any[]
|
||||
fixedParentId?: number
|
||||
showParentSelector?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const showColorPicker = ref(false)
|
||||
|
||||
// 预设颜色
|
||||
const presetColors = [
|
||||
'#FF0000', '#FF4500', '#FF8C00', '#FFD700', '#FFFF00',
|
||||
'#9ACD32', '#32CD32', '#00FF00', '#00FA9A', '#00CED1',
|
||||
'#1E90FF', '#0000FF', '#8A2BE2', '#9400D3', '#FF00FF',
|
||||
'#FF1493', '#DC143C', '#B22222', '#8B0000', '#800000',
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
parent_id: undefined as number | undefined,
|
||||
sort: 0,
|
||||
enabled: true,
|
||||
remarks: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.category?.id)
|
||||
|
||||
// 父级分类下拉搜索过滤
|
||||
const filterParentOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
|
||||
// 选择颜色
|
||||
const selectColor = (color: string) => {
|
||||
form.value.color = color
|
||||
showColorPicker.value = false
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.category && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.category.name || '',
|
||||
code: props.category.code || '',
|
||||
description: props.category.description || '',
|
||||
icon: props.category.icon || '',
|
||||
color: props.category.color || '',
|
||||
parent_id: props.category.parent_id,
|
||||
sort: props.category.sort || 0,
|
||||
enabled: props.category.enabled !== undefined ? props.category.enabled : true,
|
||||
remarks: props.category.remarks || '',
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
parent_id: props.fixedParentId,
|
||||
sort: 0,
|
||||
enabled: true,
|
||||
remarks: '',
|
||||
}
|
||||
}
|
||||
showColorPicker.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
code: form.value.code,
|
||||
description: form.value.description,
|
||||
icon: form.value.icon,
|
||||
color: form.value.color,
|
||||
parent_id: form.value.parent_id,
|
||||
sort: form.value.sort,
|
||||
enabled: form.value.enabled,
|
||||
remarks: form.value.remarks,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.category?.id) {
|
||||
// 编辑分类
|
||||
data.id = props.category.id
|
||||
res = await updateCategory(data)
|
||||
} else {
|
||||
// 新建分类
|
||||
res = await createCategory(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CategoryFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.color-picker-wrapper {
|
||||
position: relative;
|
||||
|
||||
.color-preview {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.color-picker-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
width: 280px;
|
||||
|
||||
.color-picker-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.color-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.color-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 60px;
|
||||
height: 28px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
387
src/views/ops/pages/assets/classify/index.vue
Normal file
387
src/views/ops/pages/assets/classify/index.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="资产分类管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
新增分类
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 父级分类 -->
|
||||
<template #parent="{ record }">
|
||||
{{ record.parent?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 颜色标识 -->
|
||||
<template #color="{ record }">
|
||||
<div class="color-cell">
|
||||
<div
|
||||
class="color-block"
|
||||
:style="{ backgroundColor: record.color || '#ccc' }"
|
||||
></div>
|
||||
<span>{{ record.color || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 是否启用 -->
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">
|
||||
{{ record.enabled ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="success" @click="handleAddChild(record)">
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 分类表单对话框(新增/编辑) -->
|
||||
<category-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:category="editingCategory"
|
||||
:parent-categories="parentCategories"
|
||||
:fixed-parent-id="fixedParentId"
|
||||
:show-parent-selector="showParentSelector"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 分类详情对话框 -->
|
||||
<category-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:category="currentCategory"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import {
|
||||
fetchCategoryList,
|
||||
deleteCategory,
|
||||
fetchAllCategories,
|
||||
fetchCategoryDetail,
|
||||
} from '@/api/ops/assetCategory'
|
||||
import CategoryFormDialog from './components/CategoryFormDialog.vue'
|
||||
import CategoryDetailDialog from './components/CategoryDetailDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
parent_id: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 父级分类列表(用于下拉选择)
|
||||
const parentCategories = ref<any[]>([])
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入分类名称或编码',
|
||||
},
|
||||
{
|
||||
field: 'parent_id',
|
||||
label: '父级分类',
|
||||
type: 'select',
|
||||
placeholder: '请选择父级分类',
|
||||
options: parentCategories.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
allowClear: true,
|
||||
allowSearch: true,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '分类名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '分类编码',
|
||||
dataIndex: 'code',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '父级分类',
|
||||
dataIndex: 'parent',
|
||||
slotName: 'parent',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '层级',
|
||||
dataIndex: 'level',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '颜色标识',
|
||||
dataIndex: 'color',
|
||||
slotName: 'color',
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
dataIndex: 'enabled',
|
||||
slotName: 'enabled',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'actions',
|
||||
width: 300,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的分类
|
||||
const currentCategory = ref<any>(null)
|
||||
const editingCategory = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 固定的父级分类ID(用于新增子分类时)
|
||||
const fixedParentId = ref<number | undefined>(undefined)
|
||||
|
||||
// 是否显示父级分类选择器
|
||||
const showParentSelector = ref(true)
|
||||
|
||||
// 获取父级分类列表(用于下拉选择)
|
||||
const fetchParentCategories = async () => {
|
||||
try {
|
||||
const res = await fetchAllCategories()
|
||||
parentCategories.value = res.details || []
|
||||
} catch (error) {
|
||||
console.error('获取父级分类列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
parent_id: formModel.value.parent_id || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchCategoryList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
Message.error('获取分类列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
parent_id: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchCategories()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增分类
|
||||
const handleCreate = () => {
|
||||
editingCategory.value = null
|
||||
fixedParentId.value = undefined
|
||||
showParentSelector.value = false
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 新增子分类
|
||||
const handleAddChild = (record: any) => {
|
||||
editingCategory.value = null
|
||||
fixedParentId.value = record.id
|
||||
showParentSelector.value = true
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑分类
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑分类:', record)
|
||||
editingCategory.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = async (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
try {
|
||||
const res = await fetchCategoryDetail(record.id)
|
||||
if (res.code === 0) {
|
||||
currentCategory.value = res.details
|
||||
detailVisible.value = true
|
||||
} else {
|
||||
Message.error(res.message || '获取详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类详情失败:', error)
|
||||
Message.error('获取详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
const handleDelete = async (record: any) => {
|
||||
console.log('删除分类:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除分类 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteCategory(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchCategories()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchCategories()
|
||||
fetchParentCategories()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchParentCategories()
|
||||
fetchCategories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AssetClassify',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-block {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="设备详情"
|
||||
width="900px"
|
||||
:footer="false"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="medium"
|
||||
:label-style="{ width: '140px' }"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-descriptions-item label="资产名称">
|
||||
{{ device?.asset_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="资产编号">
|
||||
{{ device?.asset_code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="资产分类">
|
||||
{{ device?.category?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="规格型号">
|
||||
{{ device?.model || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="生产厂商">
|
||||
{{ device?.manufacturer || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="序列号">
|
||||
{{ device?.serial_number || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购日期">
|
||||
{{ formatDate(device?.purchase_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="原值(元)">
|
||||
{{ formatMoney(device?.original_value) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ device?.supplier?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="质保期">
|
||||
{{ device?.warranty_period || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="质保到期日期">
|
||||
{{ formatDate(device?.warranty_expiry) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="资产状态">
|
||||
<a-tag :color="getAssetStatusColor(device?.status || '')">
|
||||
{{ getAssetStatusText(device?.status || '') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 使用信息 -->
|
||||
<a-descriptions-item label="使用部门">
|
||||
{{ device?.department || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用人">
|
||||
{{ device?.user || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-descriptions-item label="所属数据中心">
|
||||
{{ device?.datacenter?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属楼层">
|
||||
{{ device?.floor?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属机柜">
|
||||
{{ device?.rack?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="U位">
|
||||
<template v-if="device?.unit_start || device?.unit_end">
|
||||
{{ device?.unit_start || '-' }} - {{ device?.unit_end || '-' }}
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 标签信息 -->
|
||||
<a-descriptions-item label="二维码标签">
|
||||
{{ device?.qr_code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="RFID标签">
|
||||
{{ device?.rfid_tag || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="资产标签号">
|
||||
{{ device?.asset_tag || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="技术规格">
|
||||
{{ device?.specifications || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 描述信息 -->
|
||||
<a-descriptions-item label="资产描述" :span="2">
|
||||
{{ device?.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ device?.remarks || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(device?.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDateTime(device?.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ device?.created_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">
|
||||
{{ device?.updated_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
getAssetStatusText,
|
||||
getAssetStatusColor,
|
||||
} from '@/api/ops/asset'
|
||||
|
||||
interface Device {
|
||||
id?: number
|
||||
asset_name?: string
|
||||
asset_code?: string
|
||||
category?: { id: number; name: string; code: string }
|
||||
model?: string
|
||||
manufacturer?: string
|
||||
serial_number?: string
|
||||
purchase_date?: string
|
||||
original_value?: number
|
||||
supplier?: { id: number; name: string; code: string }
|
||||
warranty_period?: string
|
||||
warranty_expiry?: string
|
||||
department?: string
|
||||
user?: string
|
||||
status?: string
|
||||
location?: string
|
||||
datacenter?: { id: number; name: string; code: string }
|
||||
floor?: { id: number; name: string; code: string }
|
||||
rack?: { id: number; name: string; code: string }
|
||||
unit_start?: number
|
||||
unit_end?: number
|
||||
qr_code?: string
|
||||
rfid_tag?: string
|
||||
asset_tag?: string
|
||||
specifications?: string
|
||||
description?: string
|
||||
remarks?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
device: Device | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toLocaleString('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DeviceDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,687 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑设备' : '新增设备'"
|
||||
width="900px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-tabs v-model:active-key="activeTab">
|
||||
<!-- 基本信息 -->
|
||||
<a-tab-pane key="basic" title="基本信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="资产名称"
|
||||
field="asset_name"
|
||||
:rules="[{ required: true, message: '请输入资产名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.asset_name"
|
||||
placeholder="请输入资产名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="资产编号"
|
||||
field="asset_code"
|
||||
:rules="[{ required: true, message: '请输入资产编号' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.asset_code"
|
||||
placeholder="请输入资产编号"
|
||||
:max-length="100"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资产分类" field="category_id">
|
||||
<a-select
|
||||
v-model="form.category_id"
|
||||
placeholder="请选择资产分类"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="categoryLoading"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="规格型号" field="model">
|
||||
<a-input
|
||||
v-model="form.model"
|
||||
placeholder="请输入规格型号"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生产厂商" field="manufacturer">
|
||||
<a-input
|
||||
v-model="form.manufacturer"
|
||||
placeholder="请输入生产厂商"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="序列号" field="serial_number">
|
||||
<a-input
|
||||
v-model="form.serial_number"
|
||||
placeholder="请输入序列号"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="采购日期" field="purchase_date">
|
||||
<a-date-picker
|
||||
v-model="form.purchase_date"
|
||||
placeholder="请选择采购日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="原值(元)" field="original_value">
|
||||
<a-input-number
|
||||
v-model="form.original_value"
|
||||
placeholder="请输入原值"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="供应商" field="supplier_id">
|
||||
<a-select
|
||||
v-model="form.supplier_id"
|
||||
placeholder="请选择供应商"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="supplierLoading"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in supplierOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="质保期" field="warranty_period">
|
||||
<a-input
|
||||
v-model="form.warranty_period"
|
||||
placeholder="请输入质保期(如:36个月)"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="质保到期日期" field="warranty_expiry">
|
||||
<a-date-picker
|
||||
v-model="form.warranty_expiry"
|
||||
placeholder="请选择质保到期日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资产状态" field="status">
|
||||
<a-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择资产状态"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in assetStatusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 使用信息 -->
|
||||
<a-tab-pane key="usage" title="使用信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="使用部门" field="department">
|
||||
<a-input
|
||||
v-model="form.department"
|
||||
placeholder="请输入使用部门"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="使用人" field="user">
|
||||
<a-input
|
||||
v-model="form.user"
|
||||
placeholder="请输入使用人"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-tab-pane key="location" title="位置信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属数据中心" field="datacenter_id">
|
||||
<a-select
|
||||
v-model="form.datacenter_id"
|
||||
placeholder="请选择数据中心"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="datacenterLoading"
|
||||
@change="handleDatacenterChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in datacenterOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属楼层" field="floor_id">
|
||||
<a-select
|
||||
v-model="form.floor_id"
|
||||
placeholder="请选择楼层"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="floorLoading"
|
||||
:disabled="!form.datacenter_id"
|
||||
@change="handleFloorChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in floorOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属机柜" field="rack_id">
|
||||
<a-select
|
||||
v-model="form.rack_id"
|
||||
placeholder="请选择机柜"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="rackLoading"
|
||||
:disabled="!form.floor_id"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in rackOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="起始U位编号" field="unit_start">
|
||||
<a-input-number
|
||||
v-model="form.unit_start"
|
||||
placeholder="起始U位"
|
||||
:min="1"
|
||||
:max="48"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="结束U位编号" field="unit_end">
|
||||
<a-input-number
|
||||
v-model="form.unit_end"
|
||||
placeholder="结束U位"
|
||||
:min="1"
|
||||
:max="48"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<a-tab-pane key="other" title="其他信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="二维码标签" field="qr_code">
|
||||
<a-input
|
||||
v-model="form.qr_code"
|
||||
placeholder="请输入二维码标签"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="RFID标签" field="rfid_tag">
|
||||
<a-input
|
||||
v-model="form.rfid_tag"
|
||||
placeholder="请输入RFID标签"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资产标签号" field="asset_tag">
|
||||
<a-input
|
||||
v-model="form.asset_tag"
|
||||
placeholder="请输入资产标签号"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="技术规格" field="specifications">
|
||||
<a-input
|
||||
v-model="form.specifications"
|
||||
placeholder="请输入技术规格"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="资产描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入资产描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="1000"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" field="remarks">
|
||||
<a-textarea
|
||||
v-model="form.remarks"
|
||||
placeholder="请输入备注"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
createAsset,
|
||||
updateAsset,
|
||||
fetchCategoryOptions,
|
||||
fetchSupplierOptions,
|
||||
fetchDatacenterOptions,
|
||||
fetchFloorOptions,
|
||||
fetchRackOptions,
|
||||
assetStatusOptions,
|
||||
AssetForm,
|
||||
} from '@/api/ops/asset'
|
||||
|
||||
interface Device {
|
||||
id?: number
|
||||
asset_name?: string
|
||||
asset_code?: string
|
||||
category_id?: number
|
||||
model?: string
|
||||
manufacturer?: string
|
||||
serial_number?: string
|
||||
purchase_date?: string
|
||||
original_value?: number
|
||||
supplier_id?: number
|
||||
warranty_period?: string
|
||||
warranty_expiry?: string
|
||||
department?: string
|
||||
user?: string
|
||||
status?: string
|
||||
location?: string
|
||||
datacenter_id?: number
|
||||
floor_id?: number
|
||||
rack_id?: number
|
||||
unit_start?: number
|
||||
unit_end?: number
|
||||
qr_code?: string
|
||||
rfid_tag?: string
|
||||
asset_tag?: string
|
||||
specifications?: string
|
||||
description?: string
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
device: Device | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 下拉选项
|
||||
const categoryOptions = ref<any[]>([])
|
||||
const supplierOptions = ref<any[]>([])
|
||||
const datacenterOptions = ref<any[]>([])
|
||||
const floorOptions = ref<any[]>([])
|
||||
const rackOptions = ref<any[]>([])
|
||||
|
||||
// 加载状态
|
||||
const categoryLoading = ref(false)
|
||||
const supplierLoading = ref(false)
|
||||
const datacenterLoading = ref(false)
|
||||
const floorLoading = ref(false)
|
||||
const rackLoading = ref(false)
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.device?.id)
|
||||
|
||||
// 表单数据
|
||||
const form = ref<AssetForm>({
|
||||
asset_name: '',
|
||||
asset_code: '',
|
||||
category_id: undefined,
|
||||
model: '',
|
||||
manufacturer: '',
|
||||
serial_number: '',
|
||||
purchase_date: '',
|
||||
original_value: undefined,
|
||||
supplier_id: undefined,
|
||||
warranty_period: '',
|
||||
warranty_expiry: '',
|
||||
department: '',
|
||||
user: '',
|
||||
status: 'in_use',
|
||||
location: '',
|
||||
datacenter_id: undefined,
|
||||
floor_id: undefined,
|
||||
rack_id: undefined,
|
||||
unit_start: undefined,
|
||||
unit_end: undefined,
|
||||
qr_code: '',
|
||||
rfid_tag: '',
|
||||
asset_tag: '',
|
||||
specifications: '',
|
||||
description: '',
|
||||
remarks: '',
|
||||
})
|
||||
|
||||
// 加载下拉选项
|
||||
const loadOptions = async () => {
|
||||
// 加载资产分类
|
||||
categoryLoading.value = true
|
||||
try {
|
||||
const res = await fetchCategoryOptions()
|
||||
if (res.code === 0) {
|
||||
categoryOptions.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载资产分类失败:', error)
|
||||
} finally {
|
||||
categoryLoading.value = false
|
||||
}
|
||||
|
||||
// 加载供应商
|
||||
supplierLoading.value = true
|
||||
try {
|
||||
const res = await fetchSupplierOptions()
|
||||
if (res.code === 0) {
|
||||
supplierOptions.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载供应商失败:', error)
|
||||
} finally {
|
||||
supplierLoading.value = false
|
||||
}
|
||||
|
||||
// 加载数据中心
|
||||
datacenterLoading.value = true
|
||||
try {
|
||||
const res = await fetchDatacenterOptions()
|
||||
if (res.code === 0) {
|
||||
datacenterOptions.value = res.details?.data || res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据中心失败:', error)
|
||||
} finally {
|
||||
datacenterLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数据中心变化时加载楼层
|
||||
const handleDatacenterChange = async (value: number | undefined) => {
|
||||
form.value.floor_id = undefined
|
||||
form.value.rack_id = undefined
|
||||
floorOptions.value = []
|
||||
rackOptions.value = []
|
||||
|
||||
if (value) {
|
||||
floorLoading.value = true
|
||||
try {
|
||||
const res = await fetchFloorOptions(value)
|
||||
if (res.code === 0) {
|
||||
floorOptions.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载楼层失败:', error)
|
||||
} finally {
|
||||
floorLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 楼层变化时加载机柜
|
||||
const handleFloorChange = async (value: number | undefined) => {
|
||||
form.value.rack_id = undefined
|
||||
rackOptions.value = []
|
||||
|
||||
if (value) {
|
||||
rackLoading.value = true
|
||||
try {
|
||||
const res = await fetchRackOptions({
|
||||
datacenter_id: form.value.datacenter_id,
|
||||
floor_id: value,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
rackOptions.value = res.details?.data || res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载机柜失败:', error)
|
||||
} finally {
|
||||
rackLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 device 变化,初始化表单
|
||||
watch(
|
||||
() => props.device,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
form.value = { ...newVal } as AssetForm
|
||||
|
||||
// 如果有数据中心,加载楼层
|
||||
if (newVal.datacenter_id) {
|
||||
try {
|
||||
const res = await fetchFloorOptions(newVal.datacenter_id)
|
||||
if (res.code === 0) {
|
||||
floorOptions.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载楼层失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有楼层,加载机柜
|
||||
if (newVal.floor_id) {
|
||||
try {
|
||||
const res = await fetchRackOptions({
|
||||
datacenter_id: newVal.datacenter_id,
|
||||
floor_id: newVal.floor_id,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
rackOptions.value = res.details?.data || res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载机柜失败:', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 重置表单
|
||||
form.value = {
|
||||
asset_name: '',
|
||||
asset_code: '',
|
||||
category_id: undefined,
|
||||
model: '',
|
||||
manufacturer: '',
|
||||
serial_number: '',
|
||||
purchase_date: '',
|
||||
original_value: undefined,
|
||||
supplier_id: undefined,
|
||||
warranty_period: '',
|
||||
warranty_expiry: '',
|
||||
department: '',
|
||||
user: '',
|
||||
status: 'in_use',
|
||||
location: '',
|
||||
datacenter_id: undefined,
|
||||
floor_id: undefined,
|
||||
rack_id: undefined,
|
||||
unit_start: undefined,
|
||||
unit_end: undefined,
|
||||
qr_code: '',
|
||||
rfid_tag: '',
|
||||
asset_tag: '',
|
||||
specifications: '',
|
||||
description: '',
|
||||
remarks: '',
|
||||
}
|
||||
floorOptions.value = []
|
||||
rackOptions.value = []
|
||||
}
|
||||
activeTab.value = 'basic'
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 提交表单
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const data = { ...form.value }
|
||||
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
res = await updateAsset({ ...data, id: props.device?.id })
|
||||
} else {
|
||||
res = await createAsset(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交表单失败:', error)
|
||||
Message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DeviceFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
</style>
|
||||
400
src/views/ops/pages/assets/device/index.vue
Normal file
400
src/views/ops/pages/assets/device/index.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="设备资产管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
新增设备
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
导出
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 资产状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getAssetStatusColor(record.status)">
|
||||
{{ getAssetStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 原值 -->
|
||||
<template #original_value="{ record }">
|
||||
{{ formatMoney(record.original_value) }}
|
||||
</template>
|
||||
|
||||
<!-- 采购日期 -->
|
||||
<template #purchase_date="{ record }">
|
||||
{{ formatDate(record.purchase_date) }}
|
||||
</template>
|
||||
|
||||
<!-- 质保到期日期 -->
|
||||
<template #warranty_expiry="{ record }">
|
||||
{{ formatDate(record.warranty_expiry) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 设备表单对话框(新增/编辑) -->
|
||||
<device-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:device="editingDevice"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 设备详情对话框 -->
|
||||
<device-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:device="currentDevice"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import {
|
||||
fetchAssetList,
|
||||
deleteAsset,
|
||||
fetchAssetDetail,
|
||||
getAssetStatusText,
|
||||
getAssetStatusColor,
|
||||
assetStatusOptions,
|
||||
} from '@/api/ops/asset'
|
||||
import DeviceFormDialog from './components/DeviceFormDialog.vue'
|
||||
import DeviceDetailDialog from './components/DeviceDetailDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入资产名称/编号/部门',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '资产状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: assetStatusOptions,
|
||||
allowClear: true,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 70,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '资产编号',
|
||||
dataIndex: 'asset_code',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '资产名称',
|
||||
dataIndex: 'asset_name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '资产分类',
|
||||
dataIndex: 'category.name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '规格型号',
|
||||
dataIndex: 'model',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '使用部门',
|
||||
dataIndex: 'department',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '使用人',
|
||||
dataIndex: 'user',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '原值(元)',
|
||||
dataIndex: 'original_value',
|
||||
slotName: 'original_value',
|
||||
width: 120,
|
||||
align: 'right' as const,
|
||||
},
|
||||
{
|
||||
title: '资产状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '采购日期',
|
||||
dataIndex: 'purchase_date',
|
||||
slotName: 'purchase_date',
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'actions',
|
||||
width: 280,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的设备
|
||||
const currentDevice = ref<any>(null)
|
||||
const editingDevice = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取设备列表
|
||||
const fetchDevices = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchAssetList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
Message.error('获取设备列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchDevices()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增设备
|
||||
const handleCreate = () => {
|
||||
editingDevice.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑设备
|
||||
const handleEdit = (record: any) => {
|
||||
editingDevice.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = async (record: any) => {
|
||||
try {
|
||||
const res = await fetchAssetDetail(record.id)
|
||||
if (res.code === 0) {
|
||||
currentDevice.value = res.details
|
||||
detailVisible.value = true
|
||||
} else {
|
||||
Message.error(res.message || '获取详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备详情失败:', error)
|
||||
Message.error('获取详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除设备 ${record.asset_name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteAsset(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchDevices()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除设备失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
Message.info('正在导出...')
|
||||
const res = await fetchAssetList({
|
||||
page: 1,
|
||||
page_size: 10000,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
})
|
||||
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
const data = res.details.data
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `assets_${new Date().toISOString().slice(0, 10)}.json`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
Message.success('导出成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
Message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toLocaleString('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DeviceManage',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="供应商详情"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-tabs v-model:active-key="activeTab" v-if="supplier">
|
||||
<!-- 基本信息 -->
|
||||
<a-tab-pane key="basic" title="基本信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="供应商名称" :span="2">
|
||||
{{ supplier.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商编码">
|
||||
{{ supplier.code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="简称">
|
||||
{{ supplier.short_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商类型">
|
||||
{{ supplier.supplier_type || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商级别">
|
||||
{{ supplier.supplier_level || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(supplier.status)">
|
||||
{{ getStatusText(supplier.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商描述" :span="2">
|
||||
{{ supplier.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<a-tab-pane key="contact" title="联系信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="联系人">
|
||||
{{ supplier.contact_person || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ supplier.contact_phone || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备用联系电话">
|
||||
{{ supplier.contact_mobile || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系邮箱">
|
||||
{{ supplier.contact_email || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 地址信息 -->
|
||||
<a-tab-pane key="address" title="地址信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="省份">
|
||||
{{ supplier.province || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="城市">
|
||||
{{ supplier.city || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="邮编">
|
||||
{{ supplier.postal_code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="详细地址">
|
||||
{{ supplier.address || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 企业信息 -->
|
||||
<a-tab-pane key="company" title="企业信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="法定代表人">
|
||||
{{ supplier.legal_representative || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="注册资本">
|
||||
{{ supplier.registered_capital || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="营业执照号">
|
||||
{{ supplier.business_license || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="税号">
|
||||
{{ supplier.tax_id || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="企业类型">
|
||||
{{ supplier.company_type || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="官网地址">
|
||||
{{ supplier.website || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<a-tab-pane key="statistics" title="统计信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="订单数量">
|
||||
{{ supplier.order_count ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="累计交易金额">
|
||||
{{ formatMoney(supplier.total_amount) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="合作开始日期">
|
||||
{{ formatDate(supplier.cooperation_start_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="合作结束日期">
|
||||
{{ formatDate(supplier.cooperation_end_date) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<a-tab-pane key="system" title="系统信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(supplier.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDateTime(supplier.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ supplier.created_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">
|
||||
{{ supplier.updated_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ supplier.remarks || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Supplier {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
short_name?: string
|
||||
description?: string
|
||||
contact_person?: string
|
||||
contact_phone?: string
|
||||
contact_email?: string
|
||||
contact_mobile?: string
|
||||
province?: string
|
||||
city?: string
|
||||
address?: string
|
||||
postal_code?: string
|
||||
website?: string
|
||||
legal_representative?: string
|
||||
registered_capital?: string
|
||||
business_license?: string
|
||||
tax_id?: string
|
||||
company_type?: string
|
||||
supplier_type?: string
|
||||
supplier_level?: string
|
||||
status?: string
|
||||
order_count?: number
|
||||
total_amount?: number
|
||||
cooperation_start_date?: string
|
||||
cooperation_end_date?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
supplier: Supplier | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
paused: 'orange',
|
||||
blacklist: 'red',
|
||||
inactive: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
active: '合作中',
|
||||
paused: '暂停合作',
|
||||
blacklist: '黑名单',
|
||||
inactive: '停止合作',
|
||||
}
|
||||
return textMap[status || ''] || status || '-'
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value?: number | string | null) => {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toLocaleString('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SupplierDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-descriptions) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑供应商' : '新增供应商'"
|
||||
width="800px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-tabs v-model:active-key="activeTab">
|
||||
<!-- 基本信息 -->
|
||||
<a-tab-pane key="basic" title="基本信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="供应商名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入供应商名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入供应商名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="供应商编码"
|
||||
field="code"
|
||||
:rules="[{ required: true, message: '请输入供应商编码' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入供应商编码"
|
||||
:max-length="100"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="简称" field="short_name">
|
||||
<a-input
|
||||
v-model="form.short_name"
|
||||
placeholder="请输入简称"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="供应商类型"
|
||||
field="supplier_type"
|
||||
:rules="[{ required: true, message: '请选择供应商类型' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.supplier_type"
|
||||
placeholder="请选择供应商类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="生产商">生产商</a-option>
|
||||
<a-option value="代理商">代理商</a-option>
|
||||
<a-option value="经销商">经销商</a-option>
|
||||
<a-option value="服务商">服务商</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="供应商级别" field="supplier_level">
|
||||
<a-select
|
||||
v-model="form.supplier_level"
|
||||
placeholder="请选择供应商级别"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="A">A</a-option>
|
||||
<a-option value="B">B</a-option>
|
||||
<a-option value="C">C</a-option>
|
||||
<a-option value="D">D</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="状态"
|
||||
field="status"
|
||||
:rules="[{ required: true, message: '请选择状态' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
>
|
||||
<a-option value="active">合作中</a-option>
|
||||
<a-option value="paused">暂停合作</a-option>
|
||||
<a-option value="blacklist">黑名单</a-option>
|
||||
<a-option value="inactive">停止合作</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<a-tab-pane key="contact" title="联系信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="联系人"
|
||||
field="contact_person"
|
||||
:rules="[{ required: true, message: '请输入联系人' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.contact_person"
|
||||
placeholder="请输入联系人"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="联系电话"
|
||||
field="contact_phone"
|
||||
:rules="[{ required: true, message: '请输入联系电话' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.contact_phone"
|
||||
placeholder="请输入联系电话"
|
||||
:max-length="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="备用联系电话"
|
||||
field="contact_mobile"
|
||||
:rules="[{ required: true, message: '请输入备用联系电话' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.contact_mobile"
|
||||
placeholder="请输入备用联系电话"
|
||||
:max-length="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="联系邮箱"
|
||||
field="contact_email"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入联系邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.contact_email"
|
||||
placeholder="请输入联系邮箱"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 地址信息 -->
|
||||
<a-tab-pane key="address" title="地址信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="省份" field="province">
|
||||
<a-input
|
||||
v-model="form.province"
|
||||
placeholder="请输入省份"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="城市" field="city">
|
||||
<a-input
|
||||
v-model="form.city"
|
||||
placeholder="请输入城市"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="邮编" field="postal_code">
|
||||
<a-input
|
||||
v-model="form.postal_code"
|
||||
placeholder="请输入邮编"
|
||||
:max-length="10"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="详细地址" field="address">
|
||||
<a-input
|
||||
v-model="form.address"
|
||||
placeholder="请输入详细地址"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 企业信息 -->
|
||||
<a-tab-pane key="company" title="企业信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="法定代表人" field="legal_representative">
|
||||
<a-input
|
||||
v-model="form.legal_representative"
|
||||
placeholder="请输入法定代表人"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="注册资本" field="registered_capital">
|
||||
<a-input
|
||||
v-model="form.registered_capital"
|
||||
placeholder="请输入注册资本"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="营业执照号" field="business_license">
|
||||
<a-input
|
||||
v-model="form.business_license"
|
||||
placeholder="请输入营业执照号"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税号" field="tax_id">
|
||||
<a-input
|
||||
v-model="form.tax_id"
|
||||
placeholder="请输入税号"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="企业类型" field="company_type">
|
||||
<a-input
|
||||
v-model="form.company_type"
|
||||
placeholder="请输入企业类型"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="官网地址" field="website">
|
||||
<a-input
|
||||
v-model="form.website"
|
||||
placeholder="请输入官网地址"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="供应商描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入供应商描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createSupplier, updateSupplier } from '@/api/ops/supplier'
|
||||
|
||||
interface Supplier {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
short_name?: string
|
||||
description?: string
|
||||
contact_person?: string
|
||||
contact_phone?: string
|
||||
contact_email?: string
|
||||
contact_mobile?: string
|
||||
province?: string
|
||||
city?: string
|
||||
address?: string
|
||||
postal_code?: string
|
||||
website?: string
|
||||
legal_representative?: string
|
||||
registered_capital?: string
|
||||
business_license?: string
|
||||
tax_id?: string
|
||||
company_type?: string
|
||||
supplier_type?: string
|
||||
supplier_level?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
supplier: Supplier | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.supplier?.id)
|
||||
|
||||
// 表单数据
|
||||
const form = ref<Supplier>({
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
description: '',
|
||||
contact_person: '',
|
||||
contact_phone: '',
|
||||
contact_email: '',
|
||||
contact_mobile: '',
|
||||
province: '',
|
||||
city: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
website: '',
|
||||
legal_representative: '',
|
||||
registered_capital: '',
|
||||
business_license: '',
|
||||
tax_id: '',
|
||||
company_type: '',
|
||||
supplier_type: '',
|
||||
supplier_level: '',
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
// 监听 supplier 变化,初始化表单
|
||||
watch(
|
||||
() => props.supplier,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
form.value = { ...newVal }
|
||||
} else {
|
||||
// 重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
description: '',
|
||||
contact_person: '',
|
||||
contact_phone: '',
|
||||
contact_email: '',
|
||||
contact_mobile: '',
|
||||
province: '',
|
||||
city: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
website: '',
|
||||
legal_representative: '',
|
||||
registered_capital: '',
|
||||
business_license: '',
|
||||
tax_id: '',
|
||||
company_type: '',
|
||||
supplier_type: '',
|
||||
supplier_level: '',
|
||||
status: 'active',
|
||||
}
|
||||
}
|
||||
activeTab.value = 'basic'
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 提交表单
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const data = { ...form.value }
|
||||
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
res = await updateSupplier({ ...data, id: props.supplier?.id })
|
||||
} else {
|
||||
res = await createSupplier(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交表单失败:', error)
|
||||
Message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SupplierFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
</style>
|
||||
384
src/views/ops/pages/assets/supplier/index.vue
Normal file
384
src/views/ops/pages/assets/supplier/index.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="供应商管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
新增供应商
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 累计交易金额 -->
|
||||
<template #total_amount="{ record }">
|
||||
{{ formatMoney(record.total_amount) }}
|
||||
</template>
|
||||
|
||||
<!-- 合作开始日期 -->
|
||||
<template #cooperation_start_date="{ record }">
|
||||
{{ formatDate(record.cooperation_start_date) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 供应商表单对话框(新增/编辑) -->
|
||||
<supplier-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:supplier="editingSupplier"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 供应商详情对话框 -->
|
||||
<supplier-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:supplier="currentSupplier"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import {
|
||||
fetchSupplierList,
|
||||
deleteSupplier,
|
||||
fetchSupplierDetail,
|
||||
} from '@/api/ops/supplier'
|
||||
import SupplierFormDialog from './components/SupplierFormDialog.vue'
|
||||
import SupplierDetailDialog from './components/SupplierDetailDialog.vue'
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '合作中', value: 'active' },
|
||||
{ label: '暂停合作', value: 'paused' },
|
||||
{ label: '黑名单', value: 'blacklist' },
|
||||
{ label: '停止合作', value: 'inactive' },
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入供应商名称/编码/联系人/电话',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: statusOptions,
|
||||
allowClear: true,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '供应商编码',
|
||||
dataIndex: 'code',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '供应商名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contact_person',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contact_phone',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '订单数量',
|
||||
dataIndex: 'order_count',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '累计交易金额',
|
||||
dataIndex: 'total_amount',
|
||||
slotName: 'total_amount',
|
||||
width: 130,
|
||||
align: 'right' as const,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '合作开始日期',
|
||||
dataIndex: 'cooperation_start_date',
|
||||
slotName: 'cooperation_start_date',
|
||||
width: 130,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'actions',
|
||||
width: 200,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的供应商
|
||||
const currentSupplier = ref<any>(null)
|
||||
const editingSupplier = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取供应商列表
|
||||
const fetchSuppliers = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchSupplierList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表失败:', error)
|
||||
Message.error('获取供应商列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchSuppliers()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchSuppliers()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchSuppliers()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchSuppliers()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增供应商
|
||||
const handleCreate = () => {
|
||||
editingSupplier.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑供应商
|
||||
const handleEdit = (record: any) => {
|
||||
editingSupplier.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = async (record: any) => {
|
||||
try {
|
||||
const res = await fetchSupplierDetail(record.id)
|
||||
if (res.code === 0) {
|
||||
currentSupplier.value = res.details
|
||||
detailVisible.value = true
|
||||
} else {
|
||||
Message.error(res.message || '获取详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取供应商详情失败:', error)
|
||||
Message.error('获取详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除供应商
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除供应商 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteSupplier(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchSuppliers()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除供应商失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchSuppliers()
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
paused: 'orange',
|
||||
blacklist: 'red',
|
||||
inactive: 'gray',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
active: '合作中',
|
||||
paused: '暂停合作',
|
||||
blacklist: '黑名单',
|
||||
inactive: '停止合作',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toLocaleString('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchSuppliers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SupplierManage',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
90
src/views/ops/pages/dc/pc/components/QuickConfigDialog.vue
Normal file
90
src/views/ops/pages/dc/pc/components/QuickConfigDialog.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="快捷配置"
|
||||
:mask-closable="false"
|
||||
:ok-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="远程访问端口">
|
||||
<a-input-number
|
||||
v-model="form.remote_port"
|
||||
placeholder="请输入远程访问端口,为空则不可远程访问"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
<span style="color: #86909c">为空则不可远程访问</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Agent URL配置">
|
||||
<a-input
|
||||
v-model="form.agent_url"
|
||||
placeholder="请输入Agent URL"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
remote_port: undefined as number | undefined,
|
||||
agent_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.record) {
|
||||
form.value.remote_port = props.record.remote_port
|
||||
form.value.agent_url = props.record.agent_url || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 更新记录
|
||||
props.record.remote_port = form.value.remote_port
|
||||
props.record.agent_url = form.value.agent_url
|
||||
props.record.remote_access = !!form.value.remote_port
|
||||
props.record.agent_config = !!form.value.agent_url
|
||||
|
||||
Message.success('配置成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
Message.error('配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
@@ -74,7 +74,7 @@ export const columns = [
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
width: 180,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
|
||||
@@ -99,46 +99,59 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<!-- 操作栏 -->
|
||||
<template #actions="{ record }">
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
@@ -149,6 +162,13 @@
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -163,7 +183,8 @@ import {
|
||||
IconDesktop,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconEye
|
||||
IconEye,
|
||||
IconSettings
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
@@ -174,6 +195,7 @@ import {
|
||||
deletePC,
|
||||
} from '@/api/ops/pc'
|
||||
import ServerFormDialog from './components/ServerFormDialog.vue'
|
||||
import QuickConfigDialog from './components/QuickConfigDialog.vue'
|
||||
import ServerDetail from './components/ServerDetail.vue'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -313,6 +335,7 @@ const formModel = ref({
|
||||
})
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -440,6 +463,12 @@ const handleAdd = () => {
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑PC
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
|
||||
@@ -85,7 +85,7 @@ export const columns = [
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
width: 180,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
|
||||
@@ -108,44 +108,57 @@
|
||||
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
@@ -155,6 +168,13 @@
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -169,12 +189,14 @@ import {
|
||||
IconDesktop,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconEye
|
||||
IconEye,
|
||||
IconSettings
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import ServerFormDialog from '../pc/components/ServerFormDialog.vue'
|
||||
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchServerList,
|
||||
@@ -391,6 +413,7 @@ const mockServerData = [
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
@@ -524,6 +547,12 @@ const handleAdd = () => {
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑服务器
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
|
||||
2
src/views/ops/pages/kb/items/components/index.ts
Normal file
2
src/views/ops/pages/kb/items/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as DocumentDetailDrawer } from './DocumentDetailDrawer.vue'
|
||||
export { default as DocumentFormModal } from './DocumentFormModal.vue'
|
||||
92
src/views/ops/pages/kb/items/config/columns.ts
Normal file
92
src/views/ops/pages/kb/items/config/columns.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 表格列配置 - 知识库文档
|
||||
*/
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { getDocumentStatusText, getDocumentStatusColor, getDocumentTypeText } from './options'
|
||||
|
||||
export const getColumns = (): TableColumnData[] => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '文档编号',
|
||||
dataIndex: 'doc_no',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '文档标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '文档类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category_name',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: 'author_name',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '审核人',
|
||||
dataIndex: 'reviewer_name',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '发布时间',
|
||||
dataIndex: 'published_at',
|
||||
slotName: 'published_at',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '版本号',
|
||||
dataIndex: 'version',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '浏览次数',
|
||||
dataIndex: 'view_count',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '文档状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
]
|
||||
31
src/views/ops/pages/kb/items/config/filters.ts
Normal file
31
src/views/ops/pages/kb/items/config/filters.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 筛选项配置 - 知识库文档
|
||||
*/
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import { documentTypeOptions, documentStatusOptions } from './options'
|
||||
|
||||
export const getFilters = (): FormItem[] => [
|
||||
{
|
||||
label: '关键词',
|
||||
field: 'keyword',
|
||||
type: 'input',
|
||||
placeholder: '搜索文档标题、描述或关键字',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
label: '文档类型',
|
||||
field: 'type',
|
||||
type: 'select',
|
||||
placeholder: '请选择文档类型',
|
||||
options: documentTypeOptions,
|
||||
span: 4,
|
||||
},
|
||||
{
|
||||
label: '文档状态',
|
||||
field: 'status',
|
||||
type: 'select',
|
||||
placeholder: '请选择文档状态',
|
||||
options: documentStatusOptions,
|
||||
span: 4,
|
||||
},
|
||||
]
|
||||
62
src/views/ops/pages/kb/items/config/options.ts
Normal file
62
src/views/ops/pages/kb/items/config/options.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 选项配置 - 知识库文档
|
||||
*/
|
||||
|
||||
// 文档类型选项
|
||||
export const documentTypeOptions = [
|
||||
{ label: '通用文档', value: 'common' },
|
||||
{ label: '操作指南', value: 'guide' },
|
||||
{ label: '解决方案', value: 'solution' },
|
||||
{ label: '故障排查', value: 'troubleshoot' },
|
||||
{ label: '流程规范', value: 'process' },
|
||||
{ label: '技术文档', value: 'technical' },
|
||||
];
|
||||
|
||||
// 文档状态选项
|
||||
export const documentStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已发布', value: 'published' },
|
||||
{ label: '已审核', value: 'reviewed' },
|
||||
{ label: '未通过审核', value: 'rejected' },
|
||||
];
|
||||
|
||||
// 列表类型选项
|
||||
export const listTypeOptions = [
|
||||
{ label: '我的', value: 'mine' },
|
||||
{ label: '全部', value: 'all' },
|
||||
];
|
||||
|
||||
// 获取文档状态文本
|
||||
export const getDocumentStatusText = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
published: '已发布',
|
||||
reviewed: '已审核',
|
||||
rejected: '未通过审核',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 获取文档状态颜色
|
||||
export const getDocumentStatusColor = (status: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
draft: 'gray',
|
||||
published: 'blue',
|
||||
reviewed: 'green',
|
||||
rejected: 'red',
|
||||
};
|
||||
return colorMap[status] || 'gray';
|
||||
};
|
||||
|
||||
// 获取文档类型文本
|
||||
export const getDocumentTypeText = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
common: '通用文档',
|
||||
guide: '操作指南',
|
||||
solution: '解决方案',
|
||||
troubleshoot: '故障排查',
|
||||
process: '流程规范',
|
||||
technical: '技术文档',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
1088
src/views/ops/pages/kb/items/index.vue
Normal file
1088
src/views/ops/pages/kb/items/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
350
src/views/ops/pages/kb/recycle/index.vue
Normal file
350
src/views/ops/pages/kb/recycle/index.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['知识管理', '回收站']" />
|
||||
|
||||
<a-card class="general-card">
|
||||
<!-- 搜索表单 -->
|
||||
<SearchForm
|
||||
:model-value="searchForm"
|
||||
:form-items="filters"
|
||||
@update:model-value="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<a-divider style="margin-top: 0" />
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataTable
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
}"
|
||||
:show-download="false"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="fetchData"
|
||||
>
|
||||
<!-- 序号列 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (page - 1) * pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 资源类型列 -->
|
||||
<template #resource_type="{ record }">
|
||||
<a-tag :color="getResourceTypeColor(record.resource_type)">
|
||||
{{ getResourceTypeText(record.resource_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 删除时间列 -->
|
||||
<template #deleted_time="{ record }">
|
||||
{{ formatTime(record.deleted_time) }}
|
||||
</template>
|
||||
|
||||
<!-- 删除人列 -->
|
||||
<template #deleted_name="{ record }">
|
||||
{{ record.deleted_name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleRestore(record)">
|
||||
恢复
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleDelete(record)"
|
||||
>
|
||||
彻底删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
|
||||
<!-- 恢复确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="restoreConfirmVisible"
|
||||
title="恢复确认"
|
||||
@ok="handleConfirmRestore"
|
||||
@cancel="restoreConfirmVisible = false"
|
||||
>
|
||||
<p>
|
||||
确定要恢复「{{ recordToRestore?.resource_name }}」吗?
|
||||
</p>
|
||||
<p style="color: rgb(var(--primary-6))">恢复后资源将回到正常状态。</p>
|
||||
</a-modal>
|
||||
|
||||
<!-- 彻底删除确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="deleteConfirmVisible"
|
||||
title="彻底删除确认"
|
||||
@ok="handleConfirmDelete"
|
||||
@cancel="deleteConfirmVisible = false"
|
||||
>
|
||||
<p>
|
||||
确定要彻底删除「{{ recordToDelete?.resource_name }}」吗?
|
||||
</p>
|
||||
<p style="color: rgb(var(--danger-6))">
|
||||
警告:此操作不可恢复,删除后将无法找回!
|
||||
</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import SearchForm from '@/components/search-form/index.vue'
|
||||
import DataTable from '@/components/data-table/index.vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { TrashRecord, FetchTrashListParams } from '@/api/kb/trash'
|
||||
import {
|
||||
fetchTrashList,
|
||||
restoreTrash,
|
||||
deleteTrash,
|
||||
getResourceTypeText,
|
||||
getResourceTypeColor,
|
||||
resourceTypeOptions,
|
||||
} from '@/api/kb/trash'
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed((): TableColumnData[] => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'resource_name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'resource_type',
|
||||
slotName: 'resource_type',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '删除人',
|
||||
dataIndex: 'deleted_name',
|
||||
slotName: 'deleted_name',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '删除时间',
|
||||
dataIndex: 'deleted_time',
|
||||
slotName: 'deleted_time',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
// 搜索表单配置
|
||||
const filters = computed((): FormItem[] => [
|
||||
{
|
||||
label: '关键词',
|
||||
field: 'keyword',
|
||||
type: 'input',
|
||||
placeholder: '搜索标题',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
label: '资源类型',
|
||||
field: 'resource_type',
|
||||
type: 'select',
|
||||
placeholder: '请选择资源类型',
|
||||
options: resourceTypeOptions,
|
||||
span: 4,
|
||||
},
|
||||
])
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
resource_type: '',
|
||||
})
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (newFormModel: Record<string, any>) => {
|
||||
Object.assign(searchForm, newFormModel)
|
||||
}
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<TrashRecord[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 恢复确认
|
||||
const restoreConfirmVisible = ref(false)
|
||||
const recordToRestore = ref<TrashRecord | null>(null)
|
||||
|
||||
// 删除确认
|
||||
const deleteConfirmVisible = ref(false)
|
||||
const recordToDelete = ref<TrashRecord | null>(null)
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params: FetchTrashListParams = {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
resource_type: (searchForm.resource_type || undefined) as
|
||||
| 'document'
|
||||
| 'faq'
|
||||
| undefined,
|
||||
}
|
||||
|
||||
const res = await fetchTrashList(params)
|
||||
|
||||
if (res?.code === 200 && res.data) {
|
||||
// 如果有关键词,在前端过滤
|
||||
let data = res.data.data || []
|
||||
if (searchForm.keyword) {
|
||||
const keyword = searchForm.keyword.toLowerCase()
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.resource_name?.toLowerCase().includes(keyword) ||
|
||||
item.deleted_name?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
tableData.value = data
|
||||
total.value = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取回收站列表失败:', error)
|
||||
Message.error('获取回收站列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.resource_type = ''
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
const handlePageChange = (current: number) => {
|
||||
page.value = current
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | null): string => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||
}
|
||||
|
||||
// 恢复资源
|
||||
const handleRestore = (record: TrashRecord) => {
|
||||
recordToRestore.value = record
|
||||
restoreConfirmVisible.value = true
|
||||
}
|
||||
|
||||
// 确认恢复
|
||||
const handleConfirmRestore = async () => {
|
||||
if (!recordToRestore.value?.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await restoreTrash({ id: recordToRestore.value.id })
|
||||
if (res?.code === 200) {
|
||||
Message.success('恢复成功')
|
||||
restoreConfirmVisible.value = false
|
||||
recordToRestore.value = null
|
||||
await fetchData()
|
||||
} else {
|
||||
Message.error(res?.message || '恢复失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复失败:', error)
|
||||
Message.error('恢复失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 彻底删除
|
||||
const handleDelete = (record: TrashRecord) => {
|
||||
recordToDelete.value = record
|
||||
deleteConfirmVisible.value = true
|
||||
}
|
||||
|
||||
// 确认彻底删除
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!recordToDelete.value?.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await deleteTrash({ id: recordToDelete.value.id })
|
||||
if (res?.code === 200) {
|
||||
Message.success('彻底删除成功')
|
||||
deleteConfirmVisible.value = false
|
||||
recordToDelete.value = null
|
||||
await fetchData()
|
||||
} else {
|
||||
Message.error(res?.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
Message.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.general-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
0
src/views/ops/pages/kb/review/index.vue
Normal file
0
src/views/ops/pages/kb/review/index.vue
Normal file
324
src/views/ops/pages/kb/tags/components/CategoryFormDialog.vue
Normal file
324
src/views/ops/pages/kb/tags/components/CategoryFormDialog.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑分类' : '新增分类'"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="分类名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入分类名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="分类类型" field="type">
|
||||
<a-select
|
||||
v-model="form.type"
|
||||
placeholder="请选择分类类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="document">文档分类</a-option>
|
||||
<a-option value="faq">FAQ分类</a-option>
|
||||
<a-option value="general">通用分类</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="分类描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入分类描述"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="分类颜色" field="color">
|
||||
<div class="color-picker-wrapper">
|
||||
<a-input
|
||||
v-model="form.color"
|
||||
placeholder="请选择颜色"
|
||||
readonly
|
||||
@click="showColorPicker = !showColorPicker"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: form.color || '#ccc' }"
|
||||
></div>
|
||||
</template>
|
||||
</a-input>
|
||||
<div v-if="showColorPicker" class="color-picker-dropdown">
|
||||
<div class="color-picker-header">选择颜色</div>
|
||||
<div class="color-picker-grid">
|
||||
<div
|
||||
v-for="color in presetColors"
|
||||
:key="color"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="selectColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-picker-custom">
|
||||
<span>自定义颜色:</span>
|
||||
<input
|
||||
type="color"
|
||||
v-model="form.color"
|
||||
@change="showColorPicker = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序号" field="sort_order">
|
||||
<a-input-number
|
||||
v-model="form.sort_order"
|
||||
placeholder="请输入排序号"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注信息" field="remarks">
|
||||
<a-textarea
|
||||
v-model="form.remarks"
|
||||
placeholder="请输入备注信息"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
createCategory,
|
||||
updateCategory,
|
||||
type Category,
|
||||
} from '@/api/kb/category'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
category: Category | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const showColorPicker = ref(false)
|
||||
|
||||
// 预设颜色
|
||||
const presetColors = [
|
||||
'#FF0000', '#FF4500', '#FF8C00', '#FFD700', '#FFFF00',
|
||||
'#9ACD32', '#32CD32', '#00FF00', '#00FA9A', '#00CED1',
|
||||
'#1E90FF', '#0000FF', '#8A2BE2', '#9400D3', '#FF00FF',
|
||||
'#FF1493', '#DC143C', '#B22222', '#8B0000', '#800000',
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'document',
|
||||
color: '',
|
||||
sort_order: 0,
|
||||
remarks: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.category?.id)
|
||||
|
||||
// 选择颜色
|
||||
const selectColor = (color: string) => {
|
||||
form.value.color = color
|
||||
showColorPicker.value = false
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.category && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.category.name || '',
|
||||
description: props.category.description || '',
|
||||
type: props.category.type || 'document',
|
||||
color: props.category.color || '',
|
||||
sort_order: props.category.sort_order || 0,
|
||||
remarks: props.category.remarks || '',
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'document',
|
||||
color: '',
|
||||
sort_order: 0,
|
||||
remarks: '',
|
||||
}
|
||||
}
|
||||
showColorPicker.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
type: form.value.type,
|
||||
color: form.value.color,
|
||||
sort_order: form.value.sort_order,
|
||||
remarks: form.value.remarks,
|
||||
}
|
||||
|
||||
let res: any
|
||||
if (isEdit.value && props.category?.id) {
|
||||
// 编辑分类
|
||||
data.id = props.category.id
|
||||
res = await updateCategory(data)
|
||||
} else {
|
||||
// 新建分类
|
||||
res = await createCategory(data)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'KbCategoryFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.color-picker-wrapper {
|
||||
position: relative;
|
||||
|
||||
.color-preview {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.color-picker-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
width: 280px;
|
||||
|
||||
.color-picker-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.color-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.color-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 60px;
|
||||
height: 28px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
src/views/ops/pages/kb/tags/index.vue
Normal file
283
src/views/ops/pages/kb/tags/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
title="分类管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
新增分类
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 }}
|
||||
</template>
|
||||
|
||||
<!-- 分类类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeLabel(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 分类表单对话框(新增/编辑) -->
|
||||
<category-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:category="editingCategory"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import {
|
||||
fetchCategoryList,
|
||||
deleteCategory,
|
||||
type Category,
|
||||
} from '@/api/kb/category'
|
||||
import CategoryFormDialog from './components/CategoryFormDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<Category[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
type: '',
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '分类类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择分类类型',
|
||||
options: [
|
||||
{ label: '文档分类', value: 'document' },
|
||||
{ label: 'FAQ分类', value: 'faq' },
|
||||
{ label: '通用分类', value: 'general' },
|
||||
],
|
||||
allowClear: true,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '分类名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '分类描述',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '分类类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '备注信息',
|
||||
dataIndex: 'remarks',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'actions',
|
||||
width: 250,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的分类
|
||||
const editingCategory = ref<Category | null>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
|
||||
// 获取分类类型标签
|
||||
const getTypeLabel = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
document: '文档分类',
|
||||
faq: 'FAQ分类',
|
||||
general: '通用分类',
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取分类类型颜色
|
||||
const getTypeColor = (type: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
document: 'blue',
|
||||
faq: 'green',
|
||||
general: 'orange',
|
||||
}
|
||||
return colorMap[type] || 'gray'
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {}
|
||||
|
||||
if (formModel.value.type) {
|
||||
params.type = formModel.value.type
|
||||
}
|
||||
|
||||
const res: any = await fetchCategoryList(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
let data = res.details || []
|
||||
|
||||
// 如果有关键词搜索,进行过滤
|
||||
if (formModel.value.keyword) {
|
||||
data = data.filter((item: Category) =>
|
||||
item.name.toLowerCase().includes(formModel.value.keyword.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
tableData.value = data
|
||||
} else {
|
||||
Message.error(res.message || '获取分类列表失败')
|
||||
tableData.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
Message.error('获取分类列表失败')
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
type: '',
|
||||
}
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchCategories()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增分类
|
||||
const handleCreate = () => {
|
||||
editingCategory.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑分类
|
||||
const handleEdit = (record: Category) => {
|
||||
editingCategory.value = { ...record }
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
const handleDelete = async (record: Category) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除分类「${record.name}」吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res: any = await deleteCategory(record.id)
|
||||
if (res.code === 200) {
|
||||
Message.success('删除成功')
|
||||
fetchCategories()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
Message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'KbCategoryManage',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +1,62 @@
|
||||
<template>
|
||||
<div class="metrics-summary-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="数据源" field="data_source">
|
||||
<a-select
|
||||
v-model="formModel.data_source"
|
||||
placeholder="请选择数据源"
|
||||
style="width: 180px"
|
||||
@change="handleDataSourceChange"
|
||||
>
|
||||
<a-option value="dc-host">主机</a-option>
|
||||
<a-option value="dc-network">网络设备</a-option>
|
||||
<a-option value="dc-database">数据库</a-option>
|
||||
<a-option value="dc-middleware">中间件</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_names">
|
||||
<a-input
|
||||
v-model="formModel.metric_names"
|
||||
placeholder="多个指标名称,逗号分隔"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="标识" field="identities">
|
||||
<a-input
|
||||
v-model="formModel.identities"
|
||||
:placeholder="identityPlaceholder"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
<div>
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
@page-change="handlePageChange"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="标识" data-index="group_key" width="200" />
|
||||
<a-table-column title="指标名称" data-index="metric_name" width="180" />
|
||||
<a-table-column title="指标单位" data-index="metric_unit" width="100" />
|
||||
<a-table-column title="聚合值" data-index="value" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="样本数" data-index="sample_count" width="100" />
|
||||
<a-table-column title="聚合方式" data-index="aggregation" width="100" />
|
||||
<a-table-column title="数据源" data-index="data_source" width="120" />
|
||||
<!-- 时间范围选择器插槽 -->
|
||||
<template #form-items>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间范围" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
|
||||
<!-- 导出按钮插槽 -->
|
||||
<template #actions>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 聚合值 -->
|
||||
<template #value="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</search-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { fetchMetricsSummary, exportMetricsSummary } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
const formModel = ref<{
|
||||
data_source: string
|
||||
metric_names: string
|
||||
identities: string
|
||||
aggregation: string
|
||||
timeRange: string[]
|
||||
}>({
|
||||
data_source: 'dc-host',
|
||||
metric_names: '',
|
||||
identities: '',
|
||||
@@ -122,7 +71,7 @@ const exporting = ref(false)
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 分页
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
@@ -131,16 +80,97 @@ const pagination = reactive({
|
||||
|
||||
// 标识字段占位符
|
||||
const identityPlaceholder = computed(() => {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
if (formModel.value.data_source === 'dc-host') {
|
||||
return '服务器标识,必填'
|
||||
}
|
||||
return '服务标识,必填'
|
||||
})
|
||||
|
||||
// 处理数据源变化
|
||||
const handleDataSourceChange = () => {
|
||||
formModel.identities = ''
|
||||
}
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'data_source',
|
||||
label: '数据源',
|
||||
type: 'select',
|
||||
placeholder: '请选择数据源',
|
||||
options: [
|
||||
{ label: '主机', value: 'dc-host' },
|
||||
{ label: '网络设备', value: 'dc-network' },
|
||||
{ label: '数据库', value: 'dc-database' },
|
||||
{ label: '中间件', value: 'dc-middleware' },
|
||||
],
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'metric_names',
|
||||
label: '指标名称',
|
||||
type: 'input',
|
||||
placeholder: '多个指标名称,逗号分隔',
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'identities',
|
||||
label: '标识',
|
||||
type: 'input',
|
||||
placeholder: identityPlaceholder.value,
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'aggregation',
|
||||
label: '聚合方式',
|
||||
type: 'select',
|
||||
placeholder: '请选择聚合方式',
|
||||
options: [
|
||||
{ label: '平均值', value: 'avg' },
|
||||
{ label: '最大值', value: 'max' },
|
||||
{ label: '最小值', value: 'min' },
|
||||
{ label: '求和', value: 'sum' },
|
||||
{ label: '计数', value: 'count' },
|
||||
],
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '标识',
|
||||
dataIndex: 'group_key',
|
||||
width: 200,
|
||||
fixed: 'left' as const,
|
||||
},
|
||||
{
|
||||
title: '指标名称',
|
||||
dataIndex: 'metric_name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '指标单位',
|
||||
dataIndex: 'metric_unit',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '聚合值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
slotName: 'value',
|
||||
},
|
||||
{
|
||||
title: '样本数',
|
||||
dataIndex: 'sample_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '聚合方式',
|
||||
dataIndex: 'aggregation',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '数据源',
|
||||
dataIndex: 'data_source',
|
||||
width: 120,
|
||||
},
|
||||
])
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (value: number) => {
|
||||
@@ -151,55 +181,58 @@ const formatValue = (value: number) => {
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
data_source: formModel.data_source,
|
||||
metric_names: formModel.metric_names,
|
||||
aggregation: formModel.aggregation,
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
data_source: formModel.value.data_source,
|
||||
metric_names: formModel.value.metric_names,
|
||||
aggregation: formModel.value.aggregation,
|
||||
start_time: formModel.value.timeRange[0],
|
||||
end_time: formModel.value.timeRange[1],
|
||||
}
|
||||
|
||||
|
||||
// 根据数据源添加标识
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.identities
|
||||
if (formModel.value.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.value.identities
|
||||
} else {
|
||||
params.service_identities = formModel.identities
|
||||
params.service_identities = formModel.value.identities
|
||||
}
|
||||
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
if (!formModel.value.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_names) {
|
||||
if (!formModel.value.metric_names) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.identities) {
|
||||
if (!formModel.value.identities) {
|
||||
Message.warning('请输入标识')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
pagination.current = 1
|
||||
await fetchTableData()
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async () => {
|
||||
loading.value = true
|
||||
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
const res = await fetchMetricsSummary(params)
|
||||
|
||||
|
||||
if (res.code === 0) {
|
||||
const data = res.data?.data || []
|
||||
tableData.value = data
|
||||
tableData.value = res.data?.data || []
|
||||
pagination.total = res.data?.count || 0
|
||||
pagination.current = 1
|
||||
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
@@ -218,67 +251,77 @@ const handleSearch = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.data_source = 'dc-host'
|
||||
formModel.metric_names = ''
|
||||
formModel.identities = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.timeRange = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
pagination.current = 1
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
// 如果需要分页查询,可以在这里实现
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
data_source: 'dc-host',
|
||||
metric_names: '',
|
||||
identities: '',
|
||||
aggregation: 'avg',
|
||||
timeRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
if (!formModel.value.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_names) {
|
||||
if (!formModel.value.metric_names) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.identities) {
|
||||
if (!formModel.value.identities) {
|
||||
Message.warning('请输入标识')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
exporting.value = true
|
||||
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
params.utf8_bom = 'true'
|
||||
|
||||
|
||||
const blob = await exportMetricsSummary(params)
|
||||
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
|
||||
// 生成文件名
|
||||
const now = new Date()
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
link.download = `metrics_summary_${timestamp}.csv`
|
||||
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
|
||||
Message.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出失败:', error)
|
||||
@@ -287,6 +330,37 @@ const handleExport = async () => {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化时间范围并自动查询
|
||||
onMounted(() => {
|
||||
initDefaultTimeRange()
|
||||
// 自动查询(使用默认参数)
|
||||
formModel.value.metric_names = 'cpu_usage_percent'
|
||||
formModel.value.identities = '*'
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -296,20 +370,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.metrics-summary-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,129 +1,57 @@
|
||||
<template>
|
||||
<div class="metrics-topn-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="数据源" field="data_source">
|
||||
<a-select
|
||||
v-model="formModel.data_source"
|
||||
placeholder="请选择数据源"
|
||||
style="width: 180px"
|
||||
@change="handleDataSourceChange"
|
||||
>
|
||||
<a-option value="dc-host">主机</a-option>
|
||||
<a-option value="dc-network">网络设备</a-option>
|
||||
<a-option value="dc-database">数据库</a-option>
|
||||
<a-option value="dc-middleware">中间件</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_name">
|
||||
<a-input
|
||||
v-model="formModel.metric_name"
|
||||
placeholder="请输入指标名称"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" field="order">
|
||||
<a-select
|
||||
v-model="formModel.order"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option value="desc">降序</a-option>
|
||||
<a-option value="asc">升序</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="数量" field="limit">
|
||||
<a-input-number
|
||||
v-model="formModel.limit"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="标识" field="identities">
|
||||
<a-input
|
||||
v-model="formModel.identities"
|
||||
:placeholder="identityPlaceholder"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
<div>
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
:pagination="pagination"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank" width="80" />
|
||||
<a-table-column title="标识" data-index="group_key" width="200" />
|
||||
<a-table-column title="指标名称" data-index="metric_name" width="180" />
|
||||
<a-table-column title="指标单位" data-index="metric_unit" width="100" />
|
||||
<a-table-column title="聚合值" data-index="value" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="样本数" data-index="sample_count" width="100" />
|
||||
<a-table-column title="聚合方式" data-index="aggregation" width="100" />
|
||||
<a-table-column title="数据源" data-index="data_source" width="120" />
|
||||
<!-- 时间范围选择器插槽 -->
|
||||
<template #form-items>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间范围" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
|
||||
<!-- 聚合值 -->
|
||||
<template #value="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</search-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { fetchMetricsTopN } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
const formModel = ref<{
|
||||
data_source: string
|
||||
metric_name: string
|
||||
aggregation: string
|
||||
order: string
|
||||
limit: number
|
||||
identities: string
|
||||
timeRange: string[]
|
||||
}>({
|
||||
data_source: 'dc-host',
|
||||
metric_name: '',
|
||||
aggregation: 'avg',
|
||||
@@ -139,18 +67,134 @@ const loading = ref(false)
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 分页配置(TOPN 查询不使用分页,设置为 false)
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showTotal: false,
|
||||
showJumper: false,
|
||||
showPageSize: false,
|
||||
})
|
||||
|
||||
// 标识字段占位符
|
||||
const identityPlaceholder = computed(() => {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
if (formModel.value.data_source === 'dc-host') {
|
||||
return '多个服务器标识,逗号分隔'
|
||||
}
|
||||
return '多个服务标识,逗号分隔'
|
||||
})
|
||||
|
||||
// 处理数据源变化
|
||||
const handleDataSourceChange = () => {
|
||||
formModel.identities = ''
|
||||
}
|
||||
// 表单项配置
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
field: 'data_source',
|
||||
label: '数据源',
|
||||
type: 'select' as const,
|
||||
placeholder: '请选择数据源',
|
||||
options: [
|
||||
{ label: '主机', value: 'dc-host' },
|
||||
{ label: '网络设备', value: 'dc-network' },
|
||||
{ label: '数据库', value: 'dc-database' },
|
||||
{ label: '中间件', value: 'dc-middleware' },
|
||||
],
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'metric_name',
|
||||
label: '指标名称',
|
||||
type: 'input' as const,
|
||||
placeholder: '请输入指标名称',
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'aggregation',
|
||||
label: '聚合方式',
|
||||
type: 'select' as const,
|
||||
placeholder: '请选择聚合方式',
|
||||
options: [
|
||||
{ label: '平均值', value: 'avg' },
|
||||
{ label: '最大值', value: 'max' },
|
||||
{ label: '最小值', value: 'min' },
|
||||
{ label: '求和', value: 'sum' },
|
||||
{ label: '计数', value: 'count' },
|
||||
],
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'order',
|
||||
label: '排序',
|
||||
type: 'select' as const,
|
||||
options: [
|
||||
{ label: '降序', value: 'desc' },
|
||||
{ label: '升序', value: 'asc' },
|
||||
],
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'limit',
|
||||
label: '数量',
|
||||
type: 'input' as const,
|
||||
props: {
|
||||
min: 1,
|
||||
max: 1000,
|
||||
},
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
{
|
||||
field: 'identities',
|
||||
label: '标识',
|
||||
type: 'input' as const,
|
||||
placeholder: identityPlaceholder.value,
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: '排名',
|
||||
dataIndex: 'rank',
|
||||
width: 80,
|
||||
fixed: 'left' as const,
|
||||
},
|
||||
{
|
||||
title: '标识',
|
||||
dataIndex: 'group_key',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '指标名称',
|
||||
dataIndex: 'metric_name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '指标单位',
|
||||
dataIndex: 'metric_unit',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '聚合值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
slotName: 'value',
|
||||
},
|
||||
{
|
||||
title: '样本数',
|
||||
dataIndex: 'sample_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '聚合方式',
|
||||
dataIndex: 'aggregation',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '数据源',
|
||||
dataIndex: 'data_source',
|
||||
width: 120,
|
||||
},
|
||||
])
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (value: number) => {
|
||||
@@ -161,72 +205,130 @@ const formatValue = (value: number) => {
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
if (!formModel.value.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_name) {
|
||||
if (!formModel.value.metric_name) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
pagination.current = 1
|
||||
await fetchTableData()
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async () => {
|
||||
loading.value = true
|
||||
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
data_source: formModel.data_source,
|
||||
metric_name: formModel.metric_name,
|
||||
aggregation: formModel.aggregation,
|
||||
order: formModel.order,
|
||||
limit: formModel.limit,
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
data_source: formModel.value.data_source,
|
||||
metric_name: formModel.value.metric_name,
|
||||
aggregation: formModel.value.aggregation,
|
||||
order: formModel.value.order,
|
||||
limit: formModel.value.limit,
|
||||
start_time: formModel.value.timeRange[0],
|
||||
end_time: formModel.value.timeRange[1],
|
||||
}
|
||||
|
||||
|
||||
// 根据数据源添加标识过滤
|
||||
if (formModel.identities) {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.identities
|
||||
if (formModel.value.identities) {
|
||||
if (formModel.value.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.value.identities
|
||||
} else {
|
||||
params.service_identities = formModel.identities
|
||||
params.service_identities = formModel.value.identities
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const res = await fetchMetricsTopN(params)
|
||||
|
||||
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data?.items || []
|
||||
pagination.total = tableData.value.length
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.data_source = 'dc-host'
|
||||
formModel.metric_name = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.order = 'desc'
|
||||
formModel.limit = 10
|
||||
formModel.identities = ''
|
||||
formModel.timeRange = []
|
||||
formModel.value = {
|
||||
data_source: 'dc-host',
|
||||
metric_name: '',
|
||||
aggregation: 'avg',
|
||||
order: 'desc',
|
||||
limit: 10,
|
||||
identities: '',
|
||||
timeRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 组件挂载时自动加载
|
||||
onMounted(() => {
|
||||
initDefaultTimeRange()
|
||||
// 自动查询(使用默认参数)
|
||||
formModel.value.metric_name = 'cpu_usage_percent'
|
||||
formModel.value.data_source = 'dc-host'
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -236,20 +338,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.metrics-topn-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="网络设备状态"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@@ -68,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
@@ -338,6 +337,37 @@ const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化并自动查询
|
||||
onMounted(() => {
|
||||
initDefaultTimeRange()
|
||||
// 自动查询(使用默认参数)
|
||||
formModel.value.service_identities = '*'
|
||||
formModel.value.metric_names = 'cpu_usage_percent'
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="服务器状态"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@@ -58,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
@@ -288,6 +287,37 @@ const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化并自动查询
|
||||
onMounted(() => {
|
||||
initDefaultTimeRange()
|
||||
// 自动查询(使用默认参数)
|
||||
formModel.value.server_identities = '*'
|
||||
formModel.value.metric_names = 'cpu_usage_percent'
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,109 +1,71 @@
|
||||
<template>
|
||||
<div class="traffic-summary-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="拓扑 ID" field="topology_id">
|
||||
<a-input-number
|
||||
v-model="formModel.topology_id"
|
||||
:min="0"
|
||||
placeholder="拓扑 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="链路 ID" field="link_id">
|
||||
<a-input-number
|
||||
v-model="formModel.link_id"
|
||||
:min="0"
|
||||
placeholder="链路 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="节点 ID" field="node_ids">
|
||||
<a-input
|
||||
v-model="formModel.node_ids"
|
||||
placeholder="多个节点 ID,逗号分隔"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 汇总信息卡片 -->
|
||||
<div v-if="summaryData.totals" class="summary-cards">
|
||||
<a-card title="流量汇总" :bordered="false" class="summary-card">
|
||||
<a-descriptions :column="3" bordered>
|
||||
<a-descriptions-item v-for="(value, key) in displayTotals" :key="key" :label="formatLabel(key)">
|
||||
{{ formatValue(key, value) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 节点流量表格 -->
|
||||
<a-divider v-if="summaryData.by_node && summaryData.by_node.length > 0">节点流量明细</a-divider>
|
||||
|
||||
<a-table
|
||||
v-if="summaryData.by_node && summaryData.by_node.length > 0"
|
||||
<div>
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="summaryData.by_node"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
:pagination="pagination"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="节点 ID" data-index="node_id" width="200" fixed="left" />
|
||||
<a-table-column
|
||||
v-for="column in dynamicColumns"
|
||||
:key="String(column.key)"
|
||||
:title="column.title"
|
||||
:data-index="String(column.key)"
|
||||
:width="150"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<!-- 时间范围选择器插槽 -->
|
||||
<template #form-items>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间范围" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && !summaryData.totals" description="暂无数据" />
|
||||
|
||||
<!-- 汇总信息卡片插槽 -->
|
||||
<template #summary>
|
||||
<a-card v-if="summaryData.totals" title="流量汇总" :bordered="false" class="summary-card">
|
||||
<a-descriptions :column="3" bordered>
|
||||
<a-descriptions-item v-for="(value, key) in displayTotals" :key="key" :label="formatLabel(key)">
|
||||
{{ formatValue(key, value) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<!-- 导出按钮插槽 -->
|
||||
<template #actions>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 动态列的单元格渲染 -->
|
||||
<template v-for="column in dynamicColumns" :key="String(column.key)" #[column.slotName]="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</search-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { fetchTrafficSummary, exportTrafficReport } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
const formModel = ref<{
|
||||
topology_id: number
|
||||
link_id: number
|
||||
node_ids: string
|
||||
timeRange: string[]
|
||||
}>({
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
@@ -120,6 +82,49 @@ const summaryData = reactive<any>({
|
||||
by_node: [],
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showTotal: false,
|
||||
showJumper: false,
|
||||
showPageSize: false,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
field: 'topology_id',
|
||||
label: '拓扑 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '拓扑 ID',
|
||||
colProps: { span: 8 },
|
||||
props: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'link_id',
|
||||
label: '链路 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '链路 ID',
|
||||
colProps: { span: 8 },
|
||||
props: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'node_ids',
|
||||
label: '节点 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '多个节点 ID,逗号分隔',
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
])
|
||||
|
||||
// 显示的汇总字段(过滤掉元数据)
|
||||
const displayTotals = computed(() => {
|
||||
if (!summaryData.totals) return {}
|
||||
@@ -143,7 +148,7 @@ const dynamicColumns = computed(() => {
|
||||
if (key !== 'node_id') {
|
||||
columns.push({
|
||||
key,
|
||||
title: formatLabel(key),
|
||||
slotName: String(key),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -151,6 +156,30 @@ const dynamicColumns = computed(() => {
|
||||
return columns
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = computed(() => {
|
||||
const columns: any[] = [
|
||||
{
|
||||
title: '节点 ID',
|
||||
dataIndex: 'node_id',
|
||||
width: 200,
|
||||
fixed: 'left' as const,
|
||||
},
|
||||
]
|
||||
|
||||
// 添加动态列
|
||||
dynamicColumns.value.forEach((column) => {
|
||||
columns.push({
|
||||
title: formatLabel(column.key),
|
||||
dataIndex: String(column.key),
|
||||
width: 150,
|
||||
slotName: column.slotName,
|
||||
})
|
||||
})
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
// 格式化标签
|
||||
const formatLabel = (key: string) => {
|
||||
const labelMap: Record<string, string> = {
|
||||
@@ -209,19 +238,19 @@ const formatValue = (key: string, value: any) => {
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
start_time: formModel.value.timeRange[0],
|
||||
end_time: formModel.value.timeRange[1],
|
||||
kind: 'summary',
|
||||
}
|
||||
|
||||
if (formModel.topology_id > 0) {
|
||||
params.topology_id = formModel.topology_id
|
||||
if (formModel.value.topology_id > 0) {
|
||||
params.topology_id = formModel.value.topology_id
|
||||
}
|
||||
if (formModel.link_id > 0) {
|
||||
params.link_id = formModel.link_id
|
||||
if (formModel.value.link_id > 0) {
|
||||
params.link_id = formModel.value.link_id
|
||||
}
|
||||
if (formModel.node_ids) {
|
||||
params.node_ids = formModel.node_ids
|
||||
if (formModel.value.node_ids) {
|
||||
params.node_ids = formModel.value.node_ids
|
||||
}
|
||||
|
||||
return params
|
||||
@@ -230,11 +259,17 @@ const buildParams = () => {
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
pagination.current = 1
|
||||
await fetchTableData()
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
@@ -244,6 +279,7 @@ const handleSearch = async () => {
|
||||
if (res.code === 0) {
|
||||
summaryData.totals = res.data?.totals || null
|
||||
summaryData.by_node = res.data?.by_node || []
|
||||
pagination.total = summaryData.by_node.length
|
||||
|
||||
if (!summaryData.totals && summaryData.by_node.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
@@ -252,31 +288,51 @@ const handleSearch = async () => {
|
||||
Message.error(res.message || '查询失败')
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.topology_id = 0
|
||||
formModel.link_id = 0
|
||||
formModel.node_ids = ''
|
||||
formModel.timeRange = []
|
||||
formModel.value = {
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
timeRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
@@ -312,6 +368,35 @@ const handleExport = async () => {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化时间范围并自动查询
|
||||
onMounted(() => {
|
||||
initDefaultTimeRange()
|
||||
// 自动查询
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -321,30 +406,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.traffic-summary-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
.summary-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.summary-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,109 +1,65 @@
|
||||
<template>
|
||||
<div class="traffic-trend-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="拓扑 ID" field="topology_id">
|
||||
<a-input-number
|
||||
v-model="formModel.topology_id"
|
||||
:min="0"
|
||||
placeholder="拓扑 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="链路 ID" field="link_id">
|
||||
<a-input-number
|
||||
v-model="formModel.link_id"
|
||||
:min="0"
|
||||
placeholder="链路 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="节点 ID" field="node_ids">
|
||||
<a-input
|
||||
v-model="formModel.node_ids"
|
||||
placeholder="多个节点 ID,逗号分隔"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间粒度" field="granularity">
|
||||
<a-select
|
||||
v-model="formModel.granularity"
|
||||
placeholder="请选择时间粒度"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="minute">分钟</a-option>
|
||||
<a-option value="hour">小时</a-option>
|
||||
<a-option value="day">天</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 趋势图表 -->
|
||||
<div v-if="trendData.length > 0" class="chart-section">
|
||||
<a-card title="流量趋势图" :bordered="false">
|
||||
<div ref="chartRef" class="chart-container" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 趋势数据表格 -->
|
||||
<a-divider v-if="trendData.length > 0">趋势数据明细</a-divider>
|
||||
|
||||
<a-table
|
||||
v-if="trendData.length > 0"
|
||||
<div>
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@refresh="handleRefresh"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="时间" data-index="time" width="180" fixed="left" />
|
||||
<a-table-column title="节点 ID" data-index="node_id" width="180" />
|
||||
<a-table-column
|
||||
v-for="column in dynamicColumns"
|
||||
:key="String(column.key)"
|
||||
:title="column.title"
|
||||
:data-index="String(column.key)"
|
||||
:width="150"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<!-- 时间粒度选择器插槽 -->
|
||||
<template #form-items>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间粒度" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-select
|
||||
v-model="formModel.granularity"
|
||||
placeholder="请选择时间粒度"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="minute">分钟</a-option>
|
||||
<a-option value="hour">小时</a-option>
|
||||
<a-option value="day">天</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间范围" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && trendData.length === 0" description="暂无数据" />
|
||||
|
||||
<!-- 趋势图表插槽 -->
|
||||
<template #chart>
|
||||
<a-card v-if="trendData.length > 0" title="流量趋势图" :bordered="false">
|
||||
<div ref="chartRef" class="chart-container" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<!-- 导出按钮插槽 -->
|
||||
<template #actions>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 动态列的单元格渲染 -->
|
||||
<template v-for="column in dynamicColumns" :key="String(column.key)" #[column.slotName]="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</search-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,10 +67,17 @@
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { fetchTrafficTrend, exportTrafficReport } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
const formModel = ref<{
|
||||
topology_id: number
|
||||
link_id: number
|
||||
node_ids: string
|
||||
granularity: string
|
||||
timeRange: string[]
|
||||
}>({
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
@@ -143,6 +106,39 @@ const pagination = reactive({
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
field: 'topology_id',
|
||||
label: '拓扑 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '拓扑 ID',
|
||||
colProps: { span: 8 },
|
||||
props: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'link_id',
|
||||
label: '链路 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '链路 ID',
|
||||
colProps: { span: 8 },
|
||||
props: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'node_ids',
|
||||
label: '节点 ID',
|
||||
type: 'input' as const,
|
||||
placeholder: '多个节点 ID,逗号分隔',
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
])
|
||||
|
||||
// 动态列(从第一条记录中提取所有字段,排除 time、timestamp、node_id)
|
||||
const dynamicColumns = computed(() => {
|
||||
if (trendData.value.length === 0) return []
|
||||
@@ -154,7 +150,7 @@ const dynamicColumns = computed(() => {
|
||||
if (key !== 'time' && key !== 'timestamp' && key !== 'node_id') {
|
||||
columns.push({
|
||||
key,
|
||||
title: formatLabel(key),
|
||||
slotName: String(key),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -162,6 +158,35 @@ const dynamicColumns = computed(() => {
|
||||
return columns
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = computed(() => {
|
||||
const columns: any[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
width: 180,
|
||||
fixed: 'left' as const,
|
||||
},
|
||||
{
|
||||
title: '节点 ID',
|
||||
dataIndex: 'node_id',
|
||||
width: 180,
|
||||
},
|
||||
]
|
||||
|
||||
// 添加动态列
|
||||
dynamicColumns.value.forEach((column) => {
|
||||
columns.push({
|
||||
title: formatLabel(column.key),
|
||||
dataIndex: String(column.key),
|
||||
width: 150,
|
||||
slotName: column.slotName,
|
||||
})
|
||||
})
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
// 格式化标签
|
||||
const formatLabel = (key: string) => {
|
||||
const labelMap: Record<string, string> = {
|
||||
@@ -342,20 +367,20 @@ const handleResize = () => {
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
start_time: formModel.value.timeRange[0],
|
||||
end_time: formModel.value.timeRange[1],
|
||||
kind: 'trend',
|
||||
granularity: formModel.granularity,
|
||||
granularity: formModel.value.granularity,
|
||||
}
|
||||
|
||||
if (formModel.topology_id > 0) {
|
||||
params.topology_id = formModel.topology_id
|
||||
if (formModel.value.topology_id > 0) {
|
||||
params.topology_id = formModel.value.topology_id
|
||||
}
|
||||
if (formModel.link_id > 0) {
|
||||
params.link_id = formModel.link_id
|
||||
if (formModel.value.link_id > 0) {
|
||||
params.link_id = formModel.value.link_id
|
||||
}
|
||||
if (formModel.node_ids) {
|
||||
params.node_ids = formModel.node_ids
|
||||
if (formModel.value.node_ids) {
|
||||
params.node_ids = formModel.value.node_ids
|
||||
}
|
||||
|
||||
return params
|
||||
@@ -372,11 +397,17 @@ const updateTableData = () => {
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
pagination.current = 1
|
||||
await fetchTableData()
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
@@ -386,7 +417,6 @@ const handleSearch = async () => {
|
||||
if (res.code === 0) {
|
||||
trendData.value = res.data?.data || []
|
||||
pagination.total = trendData.value.length
|
||||
pagination.current = 1
|
||||
|
||||
if (trendData.value.length > 0) {
|
||||
updateTableData()
|
||||
@@ -418,23 +448,39 @@ const handleSearch = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.topology_id = 0
|
||||
formModel.link_id = 0
|
||||
formModel.node_ids = ''
|
||||
formModel.granularity = 'hour'
|
||||
formModel.timeRange = []
|
||||
formModel.value = {
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
granularity: 'hour',
|
||||
timeRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
trendData.value = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
pagination.current = 1
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTableData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
@@ -444,7 +490,7 @@ const handlePageChange = (current: number) => {
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
if (!formModel.value.timeRange || formModel.value.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
@@ -481,9 +527,35 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认时间范围(最近24小时)
|
||||
const initDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
const formatDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
formModel.value.timeRange = [
|
||||
formatDateTime(start),
|
||||
formatDateTime(end),
|
||||
]
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 可选:自动加载默认数据
|
||||
// 初始化默认时间范围
|
||||
initDefaultTimeRange()
|
||||
|
||||
// 自动查询
|
||||
handleSearch()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -502,29 +574,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.traffic-trend-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,32 +8,32 @@
|
||||
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
|
||||
<!-- 监测指标 TOPN -->
|
||||
<a-tab-pane key="metrics-topn" title="监测指标 TOPN">
|
||||
<metrics-topn-panel />
|
||||
<MetricsTopNPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 监测指标汇总 -->
|
||||
<a-tab-pane key="metrics-summary" title="监测指标汇总">
|
||||
<metrics-summary-panel />
|
||||
<MetricsSummaryPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 流量报表汇总 -->
|
||||
<a-tab-pane key="traffic-summary" title="流量报表汇总">
|
||||
<traffic-summary-panel />
|
||||
<TrafficSummaryPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 流量报表趋势 -->
|
||||
<a-tab-pane key="traffic-trend" title="流量报表趋势">
|
||||
<traffic-trend-panel />
|
||||
<TrafficTrendPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<a-tab-pane key="server-status" title="服务器状态">
|
||||
<server-status-panel />
|
||||
<ServerStatusPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 网络设备状态 -->
|
||||
<a-tab-pane key="network-status" title="网络设备状态">
|
||||
<network-status-panel />
|
||||
<NetworkStatusPanel />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
Reference in New Issue
Block a user