工料机、定额基价、定额费率、定额取费
This commit is contained in:
@@ -4,7 +4,9 @@ VITE_PORT=5777
|
||||
VITE_BASE=/
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL=http://127.0.0.1:48080
|
||||
#VITE_BASE_URL=http://127.0.0.1:48080
|
||||
VITE_BASE_URL=http://test.yihuiyong.cn:48080
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/admin-api
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
|
||||
238
apps/web-ele/src/api/database/materials/index.ts
Normal file
238
apps/web-ele/src/api/database/materials/index.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MaterialsApi {
|
||||
/** 工料机-分类 */
|
||||
// export interface Catalogs {
|
||||
// createTime: number,
|
||||
// updateTime: number,
|
||||
// creator: string,
|
||||
// updater: string,
|
||||
// deleted: number,
|
||||
// tenantId: number,
|
||||
// id?: number,
|
||||
// type: string
|
||||
// versionNo: string,
|
||||
// name: string
|
||||
// }
|
||||
export interface Catalogs {
|
||||
id?: number,
|
||||
parentId: number,
|
||||
code: string,
|
||||
name: string,
|
||||
nodeType: string,
|
||||
sortOrder: number,
|
||||
attributes: CatalogItemAttributes,
|
||||
children?: Catalogs[]
|
||||
}
|
||||
// export interface Acea {
|
||||
// id?: number,
|
||||
// parentId: number,
|
||||
// code: string,
|
||||
// name: string,
|
||||
// nodeType: string,
|
||||
// sortOrder: number,
|
||||
// attributes: object,
|
||||
// children:Acea[]
|
||||
// }
|
||||
export interface AddCategories {
|
||||
id?: number,
|
||||
parentId: number,
|
||||
code?: string,
|
||||
name: string,
|
||||
nodeType: string, // "specialty"
|
||||
sortOrder: number,
|
||||
}
|
||||
|
||||
/** 工料机项属性 */
|
||||
export interface CatalogItemAttributes {
|
||||
spec?: string;
|
||||
grade?: string;
|
||||
standard?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 工料机项 */
|
||||
export interface CatalogItems {
|
||||
id: number;
|
||||
catalogItemId: number;
|
||||
name: string;
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
taxRate: number;
|
||||
taxExclBasePrice: number | null;
|
||||
taxInclBasePrice: number | null;
|
||||
taxExclCompilePrice: number | null;
|
||||
taxInclCompilePrice: number | null;
|
||||
calcBase: string | null;
|
||||
isMerged: number | null;
|
||||
attributes: CatalogItemAttributes;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
categoryTreeId: number | null;
|
||||
categoryTreeName: string | null;
|
||||
}
|
||||
|
||||
export interface ResourceItems {
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
creator: string | null;
|
||||
updater: string | null;
|
||||
deleted: number;
|
||||
tenantId: number;
|
||||
id: number;
|
||||
code: string,
|
||||
name: string,
|
||||
spec?: string;//attributes?.spec
|
||||
unit: string;
|
||||
taxRate: number;
|
||||
//类别ID- ResourceItemDo.categoryId
|
||||
categoryName: string;//→从 yhy_resource_category.name 获取
|
||||
taxExclBasePrice: string;
|
||||
taxInclBasePrice: string;
|
||||
taxExclCompilePrice: string;
|
||||
taxInclCompilePrice: string;
|
||||
attributes: CatalogItemAttributes;
|
||||
// 虚拟字段(计算合价)
|
||||
taxExclBaseTotal?: number;
|
||||
taxInclBaseTotal?: number;
|
||||
taxExclCompileTotal?: number;
|
||||
taxInclCompileTotal?: number;
|
||||
}
|
||||
export interface ResourceCategories {
|
||||
code: string;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
taxExclBaseCode: string;
|
||||
taxInclBaseCode: string;
|
||||
taxExclCompileCode: string;
|
||||
taxInclCompileCode: string;
|
||||
attributes: {
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
//【第一层】资源库版本
|
||||
// export function getCatalogsList() {
|
||||
// return requestClient.get<MaterialsApi.Catalogs[]>('/core/resource/catalogs');
|
||||
// }
|
||||
|
||||
//【第二层】工料机机类树
|
||||
export function getCatalogsTreeList() {
|
||||
return requestClient.get<MaterialsApi.Catalogs[]>('/core/resource/category-tree/tree');
|
||||
}
|
||||
export function getCategoriesTree(id: any) {
|
||||
return requestClient.get<MaterialsApi.Catalogs[]>(`/core/resource/category-tree/${id}/categories`);
|
||||
}
|
||||
export function createCategoriesTree(data: MaterialsApi.AddCategories) {
|
||||
return requestClient.post('/core/resource/category-tree', data);
|
||||
}
|
||||
export function updateCategoriesTree(data:MaterialsApi.AddCategories) {
|
||||
return requestClient.put('/core/resource/category-tree', data);
|
||||
}
|
||||
export function deleteCategoriesTree(id: number) {
|
||||
return requestClient.delete(`core/resource/category-tree/${id}`);
|
||||
}
|
||||
//6.6 添加树节点与类别的关联
|
||||
export function createCategoryTreeMapping(data: any) {
|
||||
return requestClient.post('/core/resource/category-tree/mapping', data);
|
||||
}
|
||||
export function deleteCategoryTreeMapping(categoryTreeId: any, categoryId: any) {
|
||||
return requestClient.delete(`/core/resource/category-tree/mapping/${categoryTreeId}/${categoryId}`);
|
||||
}
|
||||
|
||||
|
||||
//【第三层】工料机目录树
|
||||
export function getCatalogsTreeItems(id: any) {
|
||||
return requestClient.get<MaterialsApi.Catalogs[]>(`/core/resource/catalog-items/${id}/tree`);
|
||||
}
|
||||
|
||||
// 2.2 创建工料机目录树节点
|
||||
export function createCatalogItem(data: {
|
||||
categoryTreeId: number | string;
|
||||
code: string;
|
||||
name: string;
|
||||
parentId?: number | string | null;
|
||||
sortOrder?: number;
|
||||
attributes?: any;
|
||||
}) {
|
||||
return requestClient.post<number | string>('/core/resource/catalog-items', data);
|
||||
}
|
||||
|
||||
// 2.3 更新工料机目录树节点
|
||||
export function updateCatalogItem(data: {
|
||||
id: number | string;
|
||||
categoryTreeId?: number | string;
|
||||
code?: string;
|
||||
name?: string;
|
||||
unit?: string;
|
||||
parentId?: number | string | null;
|
||||
parentPath?: string[];
|
||||
sortOrder?: number;
|
||||
attributes?: any;
|
||||
}) {
|
||||
return requestClient.put('/core/resource/catalog-items', data);
|
||||
}
|
||||
|
||||
// 2.4 删除工料机目录树节点
|
||||
export function deleteCatalogItem(id: number | string) {
|
||||
return requestClient.delete(`/core/resource/catalog-items/${id}`);
|
||||
}
|
||||
|
||||
// 2.5 获取目录树节点允许的类别列表
|
||||
export function getCatalogItemsAllowedList(id: number) {
|
||||
return requestClient.get<any[]>(`/core/resource/catalog-items/${id}/allowed-categories`);
|
||||
}
|
||||
|
||||
// 2.6 交换两个目录树节点的排序
|
||||
export function swapCatalogItemSort(nodeId1: number | string, nodeId2: number | string) {
|
||||
return requestClient.put('/core/resource/catalog-items/swap-sort', {
|
||||
nodeId1,
|
||||
nodeId2
|
||||
});
|
||||
}
|
||||
|
||||
// 【第四层】工料机项
|
||||
export function getCatalogItemsPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MaterialsApi.CatalogItems>>(
|
||||
'core/resource/items',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
//3.4 创建工料机项
|
||||
export function createResourceItems(data: any) {
|
||||
return requestClient.post('/core/resource/items', data);
|
||||
}
|
||||
//3.6 更新工料机项
|
||||
export function updateResourceItems(data: any) {
|
||||
return requestClient.put('/core/resource/items', data);
|
||||
}
|
||||
//3.7 删除工料机项
|
||||
export function deleteResourceItems(id: number) {
|
||||
return requestClient.delete(`/core/resource/items/${id}`);
|
||||
}
|
||||
|
||||
//【第五层】复合工料机价格
|
||||
export function getResourceItemsPage(params: any) {
|
||||
return requestClient.get<PageResult<MaterialsApi.ResourceItems>>(
|
||||
'/core/resource/merged/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
//5.1 创建复合工料机
|
||||
export function createResourceMerged(data: any) {
|
||||
return requestClient.post('/core/resource/merged/create', data);
|
||||
}
|
||||
//5.2 更新复合工料机
|
||||
export function updateResourceMerged(data: any) {
|
||||
return requestClient.put('/core/resource/merged/update', data);
|
||||
}
|
||||
// 5.3 删除复合工料机
|
||||
export function deleteResourceMerged(id: number) {
|
||||
return requestClient.delete(`/core/resource/merged/delete?id=${id}`);
|
||||
}
|
||||
45
apps/web-ele/src/api/database/materials/root.ts
Normal file
45
apps/web-ele/src/api/database/materials/root.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MaterialsApi {
|
||||
/** 工料机-根节点 */
|
||||
export interface Root {
|
||||
id?: number;
|
||||
code: string;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
taxExclBaseCode: string;
|
||||
taxInclBaseCode: string;
|
||||
taxExclCompileCode: string;
|
||||
taxInclCompileCode: string;
|
||||
attributes?: {
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
createTime?: number;
|
||||
updateTime?: number;
|
||||
creator?: string;
|
||||
updater?: string;
|
||||
deleted?: number;
|
||||
tenantId?: number;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//工料机类别字典(辅助数据)
|
||||
export function getCategoriesList() {
|
||||
return requestClient.get<MaterialsApi.Root[]>('/core/resource/categories');
|
||||
}
|
||||
//5.3 创建类别
|
||||
export function createCategories(data: any) {
|
||||
return requestClient.post<MaterialsApi.Root>(`/core/resource/categories`,data);
|
||||
}
|
||||
//5.4 更新类别
|
||||
export function updateCategories(data: MaterialsApi.Root) {
|
||||
return requestClient.put<MaterialsApi.Root>(`/core/resource/categories`,data);
|
||||
}
|
||||
//5.5 删除类别
|
||||
export function deleteCategories(id: number) {
|
||||
return requestClient.delete(`/core/resource/categories/${id}`);
|
||||
}
|
||||
22
apps/web-ele/src/api/database/quota/fee.ts
Normal file
22
apps/web-ele/src/api/database/quota/fee.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 3.8 查看分部分项工程费列表 */
|
||||
export function getQuotaFeeItemListWithRate(id: any) {
|
||||
return requestClient.get<any>(`/core/quota/fee/item/list-with-rate?catalogItemId=${id}`);
|
||||
}
|
||||
|
||||
/** 获取工料机类别字典(含价格代码) */
|
||||
export function getCategoriesByCatalogItem(catalogItemId: number | string) {
|
||||
return requestClient.get<any>(`/core/quota/catalog-item/${catalogItemId}/categories`);
|
||||
}
|
||||
|
||||
/** 更新取费项 */
|
||||
export function updateQuotaFeeItem(data: any) {
|
||||
return requestClient.put<any>('/core/quota/fee/item/update', data);
|
||||
}
|
||||
|
||||
/** 创建取费项 */
|
||||
export function createQuotaFeeItem(data: any) {
|
||||
return requestClient.post<any>('/core/quota/fee/item/create', data);
|
||||
}
|
||||
537
apps/web-ele/src/api/database/quota/index.ts
Normal file
537
apps/web-ele/src/api/database/quota/index.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace QuotaApi {
|
||||
/** 定额目录条目属性 */
|
||||
export interface CatalogItemAttributes {
|
||||
node_type?: string; // 'specialty'
|
||||
specialty_locked?: boolean;
|
||||
remark?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 定额专业节点(第一层) */
|
||||
export interface CatalogItem {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
parentId?: number | null; // 支持树形结构
|
||||
sortOrder: number;
|
||||
categoryTreeId?: number | null; // 绑定的工料机专业ID
|
||||
attributes: CatalogItemAttributes;
|
||||
children?: CatalogItem[]; // 子节点(树形结构)
|
||||
}
|
||||
|
||||
/** 定额子目树节点属性 */
|
||||
export interface CatalogTreeAttributes {
|
||||
content_type?: string; // 'directory' | 'content'
|
||||
remark?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 定额子目树节点(第二层) */
|
||||
export interface CatalogTreeNode {
|
||||
id: number | string; // 支持大整数,使用字符串
|
||||
catalogItemId: number; // 关联第一层定额专业
|
||||
code: string;
|
||||
name: string;
|
||||
parentId: number | string | null; // 支持大整数,使用字符串
|
||||
sortOrder: number;
|
||||
unit?: string;
|
||||
attributes: CatalogTreeAttributes;
|
||||
children?: CatalogTreeNode[];
|
||||
}
|
||||
|
||||
/** 定额子目 */
|
||||
export interface QuotaItem {
|
||||
id: number;
|
||||
catalogItemId: number;
|
||||
unit: string;
|
||||
basePrice: number;
|
||||
attributes: any;
|
||||
resources?: QuotaResource[];
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
/** 定额工料机组成 */
|
||||
export interface QuotaResource {
|
||||
id: number;
|
||||
quotaItemId: number;
|
||||
resourceItemId: number;
|
||||
resourceCode: string;
|
||||
resourceName: string;
|
||||
resourceUnit: string;
|
||||
resourcePrice: number;
|
||||
dosage: number;
|
||||
attributes: any;
|
||||
isMerged: any;
|
||||
mergedItems: any;
|
||||
}
|
||||
|
||||
/** 创建定额目录条目参数 */
|
||||
export interface CreateCatalogItemParams {
|
||||
code: string;
|
||||
name: string;
|
||||
parentId?: number | null;
|
||||
sortOrder?: number;
|
||||
unit?: string;
|
||||
attributes?: CatalogItemAttributes;
|
||||
}
|
||||
|
||||
/** 更新定额目录条目参数 */
|
||||
export interface UpdateCatalogItemParams {
|
||||
id: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
sortOrder?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
/** 绑定工料机专业参数 */
|
||||
export interface BindSpecialtyParams {
|
||||
catalogItemId: number | string; // 支持雪花ID(字符串)
|
||||
categoryTreeId: number | string; // 支持雪花ID(字符串)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 定额专业管理(第一层) ====================
|
||||
|
||||
/** 1.4 获取定额专业节点详情 */
|
||||
export function getQuotaCatalogItem(id: number | string) {
|
||||
return requestClient.get<QuotaApi.CatalogItem>(
|
||||
`/core/quota/catalog-item/get?id=${id}`
|
||||
);
|
||||
}
|
||||
|
||||
/** 1.2 获取定额专业树结构 */
|
||||
export function getQuotaCatalogItemTree(params?: { exclude?: string }) {
|
||||
return requestClient.get<QuotaApi.CatalogItem[]>(
|
||||
'/core/quota/catalog-item/tree',
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
/** 1.5 获取定额专业节点列表 */
|
||||
export function getQuotaCatalogItemList() {
|
||||
return requestClient.get<QuotaApi.CatalogItem[]>(
|
||||
'/core/quota/catalog-item/list'
|
||||
);
|
||||
}
|
||||
|
||||
/** 1.1 创建定额专业节点 */
|
||||
export function createQuotaCatalogItem(data: QuotaApi.CreateCatalogItemParams) {
|
||||
return requestClient.post('/core/quota/catalog-item/create', data);
|
||||
}
|
||||
|
||||
/** 1.2 更新定额专业节点 */
|
||||
export function updateQuotaCatalogItem(data: QuotaApi.UpdateCatalogItemParams) {
|
||||
return requestClient.put('/core/quota/catalog-item/update', data);
|
||||
}
|
||||
|
||||
/** 1.3 删除定额专业节点 */
|
||||
export function deleteQuotaCatalogItem(id: number | string) {
|
||||
return requestClient.delete(`/core/quota/catalog-item/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 1.6 绑定工料机专业 */
|
||||
export function bindQuotaSpecialty(data: QuotaApi.BindSpecialtyParams) {
|
||||
return requestClient.post('/core/quota/catalog-item/bind-specialty', data);
|
||||
}
|
||||
|
||||
// ==================== 定额子目树管理(第二层) ====================
|
||||
|
||||
/** 2.4 获取定额子目树节点详情 */
|
||||
export function getQuotaCatalogTreeNode(id: number | string) {
|
||||
return requestClient.get<QuotaApi.CatalogTreeNode>(
|
||||
`/core/quota/catalog-tree/get?id=${id}`
|
||||
);
|
||||
}
|
||||
|
||||
/** 2.5 获取定额子目树节点列表 */
|
||||
export function getQuotaCatalogTreeList(catalogItemId: number | string) {
|
||||
return requestClient.get<QuotaApi.CatalogTreeNode[]>(
|
||||
'/core/quota/catalog-tree/list',
|
||||
{ params: { catalogItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 2.6 获取定额子目树结构 */
|
||||
export function getQuotaCatalogTree(catalogItemId: number | string) {
|
||||
return requestClient.get<QuotaApi.CatalogTreeNode[]>(
|
||||
'/core/quota/catalog-tree/tree',
|
||||
{ params: { catalogItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 2.1 创建定额子目树节点 */
|
||||
export function createQuotaCatalogTreeNode(data: {
|
||||
catalogItemId: number;
|
||||
parentId?: number | string | null; // 支持大整数,使用字符串
|
||||
code: string;
|
||||
name: string;
|
||||
unit?: string;
|
||||
contentType?: 'directory' | 'content';
|
||||
sortOrder?: number;
|
||||
attributes?: any;
|
||||
}) {
|
||||
return requestClient.post<number | string>('/core/quota/catalog-tree/create', data);
|
||||
}
|
||||
|
||||
/** 2.2 更新定额子目树节点 */
|
||||
export function updateQuotaCatalogTreeNode(data: {
|
||||
id: number;
|
||||
catalogItemId?: number;
|
||||
parentId?: number | null;
|
||||
code?: string;
|
||||
name?: string;
|
||||
unit?: string;
|
||||
contentType?: 'directory' | 'content';
|
||||
sortOrder?: number;
|
||||
attributes?: any;
|
||||
}) {
|
||||
return requestClient.put('/core/quota/catalog-tree/update', data);
|
||||
}
|
||||
|
||||
/** 2.3 删除定额子目树节点 */
|
||||
export function deleteQuotaCatalogTreeNode(id: number | string) {
|
||||
return requestClient.delete(`/core/quota/catalog-tree/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 2.7 交换两个节点的排序 */
|
||||
export function swapQuotaCatalogTreeSort(nodeId1: number | string, nodeId2: number | string) {
|
||||
return requestClient.put('/core/quota/catalog-tree/swap-sort', {
|
||||
nodeId1,
|
||||
nodeId2
|
||||
});
|
||||
}
|
||||
|
||||
/** 2.8 根据费率模式节点查询子目录树 */
|
||||
export function getQuotaCatalogTreeByRateModeNode(rateModeNodeId: number | string) {
|
||||
return requestClient.get<QuotaApi.CatalogTreeNode[]>(
|
||||
'/core/quota/catalog-tree/list-by-rate-mode-node',
|
||||
{ params: { rateModeNodeId } }
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 定额子目管理(第四层) ====================
|
||||
|
||||
/** 2.4 获取定额子目详情 */
|
||||
export function getQuotaItem(id: number) {
|
||||
return requestClient.get<QuotaApi.QuotaItem>(
|
||||
`/core/quota/item/get?id=${id}`
|
||||
);
|
||||
}
|
||||
|
||||
/** 2.5 获取定额子目列表 */
|
||||
export function getQuotaItemList(catalogItemId: number | string) {
|
||||
return requestClient.get<QuotaApi.QuotaItem[]>(
|
||||
'/core/quota/item/list',
|
||||
{ params: { catalogItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 2.1 创建定额子目 */
|
||||
export function createQuotaItem(data: any) {
|
||||
return requestClient.post('/core/quota/item/create', data);
|
||||
}
|
||||
|
||||
/** 2.2 更新定额子目 */
|
||||
export function updateQuotaItem(data: any) {
|
||||
return requestClient.put('/core/quota/item/update', data);
|
||||
}
|
||||
|
||||
/** 2.3 删除定额子目 */
|
||||
export function deleteQuotaItem(id: number) {
|
||||
return requestClient.delete(`/core/quota/item/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 2.6 计算定额基价 */
|
||||
export function calculateQuotaPrice(id: number) {
|
||||
return requestClient.post(`/core/quota/item/calculate-price?id=${id}`);
|
||||
}
|
||||
|
||||
// ==================== 定额工料机组成管理 ====================
|
||||
|
||||
/** 3.4 获取定额工料机组成列表 */
|
||||
export function getQuotaResourceList(quotaItemId: number) {
|
||||
return requestClient.get<QuotaApi.QuotaResource[]>(
|
||||
'/core/quota/resource/list',
|
||||
{ params: { quotaItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 3.5 获取可选的工料机列表(已过滤范围,支持模糊查询) */
|
||||
export function getAvailableResourceList(quotaItemId: number, params?: { code?: string; name?: string; spec?: string }) {
|
||||
return requestClient.get<any[]>(
|
||||
'/core/quota/resource/available-list',
|
||||
{ params: { quotaItemId, ...params } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 3.1 添加定额工料机组成 */
|
||||
export function createQuotaResource(data: any) {
|
||||
return requestClient.post('/core/quota/resource/create', data);
|
||||
}
|
||||
|
||||
/** 3.2 更新定额工料机组成 */
|
||||
export function updateQuotaResource(data: any) {
|
||||
return requestClient.put('/core/quota/resource/update', data);
|
||||
}
|
||||
|
||||
/** 3.3 删除定额工料机组成 */
|
||||
export function deleteQuotaResource(id: number) {
|
||||
return requestClient.delete(`/core/quota/resource/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// ==================== 定额调整设置管理(第五层) ====================
|
||||
|
||||
/** 5.1 创建调整设置 */
|
||||
export function createAdjustmentSetting(data: any) {
|
||||
return requestClient.post('/core/quota/adjustment-setting/create', data);
|
||||
}
|
||||
|
||||
/** 5.2 更新调整设置 */
|
||||
export function updateAdjustmentSetting(data: any) {
|
||||
return requestClient.put('/core/quota/adjustment-setting/update', data);
|
||||
}
|
||||
|
||||
/** 5.3 删除调整设置 */
|
||||
export function deleteAdjustmentSetting(id: number) {
|
||||
return requestClient.delete(`/core/quota/adjustment-setting/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 5.4 获取调整设置详情 */
|
||||
export function getAdjustmentSetting(id: number) {
|
||||
return requestClient.get<any>(`/core/quota/adjustment-setting/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 5.5 获取调整设置列表 */
|
||||
export function getAdjustmentSettingList(quotaItemId: number) {
|
||||
return requestClient.get<any[]>(
|
||||
'/core/quota/adjustment-setting/list',
|
||||
{ params: { quotaItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 5.6 交换排序 */
|
||||
export function swapAdjustmentSettingSort(id1: number, id2: number) {
|
||||
return requestClient.post('/core/quota/adjustment-setting/swap-sort', {
|
||||
id1,
|
||||
id2
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 定额调整明细管理(第六层) ====================
|
||||
|
||||
/** 6.1 创建调整明细 */
|
||||
export function createAdjustmentDetail(data: any) {
|
||||
return requestClient.post('/core/quota/adjustment-detail/create', data);
|
||||
}
|
||||
|
||||
/** 6.2 更新调整明细 */
|
||||
export function updateAdjustmentDetail(data: any) {
|
||||
return requestClient.put('/core/quota/adjustment-detail/update', data);
|
||||
}
|
||||
|
||||
/** 6.3 删除调整明细 */
|
||||
export function deleteAdjustmentDetail(id: number) {
|
||||
return requestClient.delete(`/core/quota/adjustment-detail/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 6.4 获取调整明细详情 */
|
||||
export function getAdjustmentDetail(id: number) {
|
||||
return requestClient.get<any>(`/core/quota/adjustment-detail/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 6.5 获取调整明细列表 */
|
||||
export function getAdjustmentDetailList(quotaItemId: number) {
|
||||
return requestClient.get<any[]>(
|
||||
'/core/quota/adjustment-detail/list',
|
||||
{ params: { quotaItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 6.6 交换排序 */
|
||||
export function swapAdjustmentDetailSort(id1: number, id2: number) {
|
||||
return requestClient.post('/core/quota/adjustment-detail/swap-sort', {
|
||||
id1,
|
||||
id2
|
||||
});
|
||||
}
|
||||
|
||||
/** 6.8 获取调整设置与明细的组合列表 */
|
||||
export function getAdjustmentCombinedList(quotaItemId: number) {
|
||||
return requestClient.get<any[]>(
|
||||
'/core/quota/adjustment-detail/combined-list',
|
||||
{ params: { quotaItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 定额费率管理 ====================
|
||||
|
||||
/** 7.1 创建费率模式节点 */
|
||||
export function createRateModeNode(data: {
|
||||
parentId: number | string;
|
||||
code: string;
|
||||
name: string;
|
||||
sortOrder?: number;
|
||||
}) {
|
||||
return requestClient.post<number | string>(
|
||||
'/core/quota/rate/item/create-mode-node',
|
||||
data
|
||||
);
|
||||
}
|
||||
/** 3.8 查看分部分项工程费列表 */
|
||||
export function getQuotaRateItemTree(id: any) {
|
||||
return requestClient.get<any>(`/core/quota/rate/item/tree?catalogItemId=${id}`);
|
||||
}
|
||||
|
||||
/** 7.2 创建费率项 */
|
||||
export function createRateItem(data: any) {
|
||||
return requestClient.post('/core/quota/rate/item/create', data);
|
||||
}
|
||||
|
||||
/** 7.3 更新费率项 */
|
||||
export function updateRateItem(data: any) {
|
||||
return requestClient.put('/core/quota/rate/item/update', data);
|
||||
}
|
||||
|
||||
/** 7.4 删除费率项 */
|
||||
export function deleteRateItem(id: number | string) {
|
||||
return requestClient.delete(`/core/quota/rate/item/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 7.5 获取费率项详情 */
|
||||
export function getRateItem(id: number | string) {
|
||||
return requestClient.get<any>(`/core/quota/rate/item/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 7.6 配置动态取值规则 */
|
||||
export function configValueRules(data: {
|
||||
rateItemId: number | string;
|
||||
tiers?: Array<{
|
||||
seq: number;
|
||||
threshold: number;
|
||||
compareType: string;
|
||||
fieldValues: Record<string, number>;
|
||||
}>;
|
||||
increments?: Array<{
|
||||
seq: number;
|
||||
step: number;
|
||||
baseThreshold: number;
|
||||
fieldIncrements: Record<string, number>;
|
||||
}>;
|
||||
}) {
|
||||
return requestClient.post('/core/quota/rate/item/config-value-rules', data);
|
||||
}
|
||||
|
||||
/** 7.7 计算动态字段值 */
|
||||
export function calculateDynamicFields(rateItemId: number | string, baseValue: number) {
|
||||
return requestClient.get<Record<number, number>>(
|
||||
'/core/quota/rate/item/calculate-fields',
|
||||
{ params: { rateItemId, baseValue } }
|
||||
);
|
||||
}
|
||||
|
||||
/** 7.8 交换费率项排序 */
|
||||
export function swapRateItemSort(nodeId1: number | string, nodeId2: number | string) {
|
||||
return requestClient.post('/core/quota/rate/item/swap-sort', {
|
||||
nodeId1,
|
||||
nodeId2
|
||||
});
|
||||
}
|
||||
|
||||
/** 7.9 移动费率项到指定位置 */
|
||||
export function moveRateItemNode(nodeId: number | string, targetNodeId: number | string) {
|
||||
return requestClient.post('/core/quota/rate/item/move-node', {
|
||||
nodeId1: nodeId,
|
||||
nodeId2: targetNodeId
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 定额费率字段管理 ====================
|
||||
|
||||
/** 8.1 保存费率字段 */
|
||||
export function saveRateField(data: {
|
||||
id?: number | string;
|
||||
catalogItemId: number | string;
|
||||
rateItemId?: number | string;
|
||||
fieldIndex: number;
|
||||
fieldValue?: number;
|
||||
fieldLabelId?: number | string;
|
||||
bindingIds?: Array<number | string>;
|
||||
}) {
|
||||
return requestClient.post('/core/quota/rate/field/save', data);
|
||||
}
|
||||
|
||||
/** 8.2 更新字段绑定 */
|
||||
export function updateFieldBinding(
|
||||
catalogItemId: number | string,
|
||||
fieldIndex: number,
|
||||
bindingIds: Array<number | string>
|
||||
) {
|
||||
return requestClient.post('/core/quota/rate/field/update-binding', bindingIds, {
|
||||
params: { catalogItemId, fieldIndex }
|
||||
});
|
||||
}
|
||||
|
||||
/** 8.3 获取费率字段列表 */
|
||||
export function getRateFieldList(catalogItemId: number | string) {
|
||||
return requestClient.get<any[]>(
|
||||
'/core/quota/rate/field/list',
|
||||
{ params: { catalogItemId } }
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 字段标签字典管理 ====================
|
||||
|
||||
/** 9.1 创建字段标签 */
|
||||
export function createFieldLabel(data: {
|
||||
catalogItemId: number | string;
|
||||
labelName: string;
|
||||
}) {
|
||||
return requestClient.post<number>('/core/quota/rate-field-label/create', data);
|
||||
}
|
||||
|
||||
/** 9.2 更新字段标签 */
|
||||
export function updateFieldLabel(data: {
|
||||
id: number | string;
|
||||
catalogItemId: number | string;
|
||||
labelName: string;
|
||||
}) {
|
||||
return requestClient.put<boolean>('/core/quota/rate-field-label/update', data);
|
||||
}
|
||||
|
||||
/** 9.3 删除字段标签 */
|
||||
export function deleteFieldLabel(id: number | string) {
|
||||
return requestClient.delete('/core/quota/rate-field-label/delete', {
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
/** 9.4 获取字段标签详情 */
|
||||
export function getFieldLabel(id: number | string) {
|
||||
return requestClient.get('/core/quota/rate-field-label/get', {
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
/** 9.5 获取字段标签列表 */
|
||||
export function getFieldLabelList(catalogItemId: number | string) {
|
||||
return requestClient.get<Array<{
|
||||
id: number;
|
||||
catalogItemId: number;
|
||||
labelName: string;
|
||||
sortOrder: number;
|
||||
}>>('/core/quota/rate-field-label/list', {
|
||||
params: { catalogItemId }
|
||||
});
|
||||
}
|
||||
|
||||
/** 9.6 交换字段标签排序 */
|
||||
export function swapFieldLabelSort(labelId1: number | string, labelId2: number | string) {
|
||||
return requestClient.post('/core/quota/rate-field-label/swap-sort', null, {
|
||||
params: { labelId1, labelId2 }
|
||||
});
|
||||
}
|
||||
1
apps/web-ele/src/components/db-calc/index.ts
Normal file
1
apps/web-ele/src/components/db-calc/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DbCalc } from './index.vue';
|
||||
252
apps/web-ele/src/components/db-calc/index.vue
Normal file
252
apps/web-ele/src/components/db-calc/index.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { ElButton, ElDialog, ElInput } from 'element-plus'
|
||||
import Handsontable from 'handsontable'
|
||||
import 'handsontable/dist/handsontable.full.css'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
interface TableColumn {
|
||||
prop: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
currentValue?: string
|
||||
tableData?: any[]
|
||||
tableColumn?: TableColumn[]
|
||||
tableProp?: string // 指定点击单元格时获取哪个字段的值
|
||||
mode?: 'resource' | 'fee' // 模式:resource=工料机(返回类别ID),fee=定额取费(返回对象)
|
||||
}
|
||||
|
||||
interface ConfirmResult {
|
||||
formula: string
|
||||
variables: Record<string, number | { categoryId: number; priceField: string }>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', result: ConfirmResult): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
currentValue: '',
|
||||
tableData: () => [],
|
||||
tableColumn: () => [],
|
||||
tableProp: 'code', // 默认使用 code 字段
|
||||
mode: 'resource' // 默认为工料机模式
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Handsontable 相关
|
||||
const tableContainer = ref<HTMLElement | null>(null)
|
||||
let hotInstance: Handsontable | null = null
|
||||
|
||||
// 计算基数弹窗数据 - 使用 computed 从 props 获取
|
||||
const calcTableData = computed(() => props.tableData || [])
|
||||
|
||||
// 运算符号按钮
|
||||
const operators = ['+', '-', '*', '/', '(', ')']
|
||||
|
||||
// 当前编辑的值
|
||||
const editValue = ref<string>('')
|
||||
|
||||
// 监听 props.currentValue 变化
|
||||
watch(() => props.currentValue, (newVal) => {
|
||||
editValue.value = newVal || ''
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听弹窗关闭,清空编辑值
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (!newVal) {
|
||||
editValue.value = ''
|
||||
if (hotInstance) {
|
||||
hotInstance.destroy()
|
||||
hotInstance = null
|
||||
}
|
||||
} else {
|
||||
editValue.value = props.currentValue || ''
|
||||
// 弹窗打开时初始化表格
|
||||
nextTick(() => {
|
||||
initHandsontable()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化 Handsontable
|
||||
const initHandsontable = () => {
|
||||
if (!tableContainer.value || hotInstance) return
|
||||
|
||||
// 转换列配置
|
||||
const columns = props.tableColumn.map(col => ({
|
||||
data: col.prop,
|
||||
title: col.label,
|
||||
readOnly: true
|
||||
}))
|
||||
|
||||
console.log('=== Handsontable 初始化 ===')
|
||||
console.log('表格数据:', calcTableData.value)
|
||||
console.log('列配置:', columns)
|
||||
console.log('原始列配置:', props.tableColumn)
|
||||
|
||||
hotInstance = new Handsontable(tableContainer.value, {
|
||||
data: calcTableData.value,
|
||||
columns: columns,
|
||||
colHeaders: true,
|
||||
rowHeaders: false,
|
||||
height: 300,
|
||||
licenseKey: 'non-commercial-and-evaluation',
|
||||
readOnly: true,
|
||||
afterOnCellMouseDown: (_event: MouseEvent, coords: { row: number; col: number }) => {
|
||||
if (coords.row >= 0 && coords.col >= 0) {
|
||||
const rowData = hotInstance?.getSourceDataAtRow(coords.row) as any
|
||||
const colProp = props.tableColumn[coords.col]?.prop
|
||||
|
||||
// 跳过序号列和类别列(不允许点击)
|
||||
if (colProp === 'id' || colProp === 'name') {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 tableProp 指定的字段获取值
|
||||
const targetProp = props.tableProp
|
||||
const value = rowData?.[targetProp]
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
const cellValue = Array.isArray(value) ? value.join('') : String(value)
|
||||
editValue.value += cellValue
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加运算符
|
||||
const addOperator = (op: string) => {
|
||||
editValue.value += op
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 提取公式中使用的变量,构建 variables 对象
|
||||
const extractVariables = (formula: string): Record<string, number | { categoryId: number; priceField: string }> => {
|
||||
const variables: Record<string, number | { categoryId: number; priceField: string }> = {}
|
||||
|
||||
if (!formula || !props.tableData || props.tableData.length === 0) {
|
||||
return variables
|
||||
}
|
||||
|
||||
// 创建正则表达式,将运算符和数字替换为空格,用于分割
|
||||
const operatorPattern = operators.map(op => {
|
||||
// 转义特殊字符
|
||||
return op.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}).join('|')
|
||||
|
||||
// 移除运算符、数字、小数点和空格,剩下的就是变量代码
|
||||
const cleanFormula = formula.replace(new RegExp(`(${operatorPattern}|\\d+\\.?\\d*|\\s+)`, 'g'), ' ')
|
||||
|
||||
// 分割出所有可能的变量代码
|
||||
const potentialCodes = cleanFormula.split(/\s+/).filter(code => code.trim() !== '')
|
||||
|
||||
// 价格字段类型映射
|
||||
const priceFieldMap: Record<string, string> = {
|
||||
'taxExclBaseCode': 'tax_excl_base_price',
|
||||
'taxInclBaseCode': 'tax_incl_base_price',
|
||||
'taxExclCompileCode': 'tax_excl_compile_price',
|
||||
'taxInclCompileCode': 'tax_incl_compile_price'
|
||||
}
|
||||
|
||||
// 遍历 tableData,检查公式中是否包含该项的所有价格代码
|
||||
props.tableData.forEach((item: any) => {
|
||||
// 检查所有可能的价格代码字段
|
||||
const priceCodeFields = ['taxExclBaseCode', 'taxInclBaseCode', 'taxExclCompileCode', 'taxInclCompileCode', 'code']
|
||||
|
||||
priceCodeFields.forEach(field => {
|
||||
const codeValue = item[field]
|
||||
if (codeValue && potentialCodes.includes(codeValue)) {
|
||||
if (item.id !== undefined) {
|
||||
// 根据模式返回不同格式
|
||||
if (props.mode === 'resource') {
|
||||
// 工料机模式:直接返回类别ID
|
||||
variables[codeValue] = item.id
|
||||
} else {
|
||||
// 定额取费模式:返回对象 { categoryId, priceField }
|
||||
const priceFieldType = priceFieldMap[field] || 'tax_excl_base_price'
|
||||
variables[codeValue] = {
|
||||
categoryId: item.id,
|
||||
priceField: priceFieldType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
// 保存计算数据
|
||||
const handleConfirm = () => {
|
||||
const variables = extractVariables(editValue.value)
|
||||
|
||||
const result: ConfirmResult = {
|
||||
formula: editValue.value,
|
||||
variables: variables
|
||||
}
|
||||
|
||||
// 只触发 confirm 事件,不自动关闭弹窗
|
||||
// 让父组件在保存成功后再关闭弹窗
|
||||
emit('confirm', result)
|
||||
// 移除自动关闭逻辑
|
||||
// emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
title="计算基数设置"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="margin-bottom: 10px; color: #909399; font-size: 13px;">
|
||||
限数字:0-9及小数点。运算符号:+加、-减、*乘、/除、()英文括号,代码。
|
||||
</div>
|
||||
<ElInput
|
||||
size="large"
|
||||
v-model="editValue"
|
||||
style="width: 100%;"
|
||||
placeholder="点击表格单元格或运算符按钮添加内容"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<span style="margin-right: 10px;">运算符号:</span>
|
||||
<ElButton
|
||||
v-for="op in operators"
|
||||
:key="op"
|
||||
size="large"
|
||||
@click="addOperator(op)"
|
||||
style="margin-right: 5px; font-size: 20px;"
|
||||
>
|
||||
{{ op }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div ref="tableContainer" style="width: 100%; margin-bottom: 15px;"></div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleClose">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
75
apps/web-ele/src/components/db-hst/README.md
Normal file
75
apps/web-ele/src/components/db-hst/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 右键菜单组件使用说明
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 全局菜单管理
|
||||
- 当打开一个右键菜单时,会自动关闭其他已打开的菜单
|
||||
- 支持多个菜单组件实例共存,互不干扰
|
||||
- 自动清理资源,防止内存泄漏
|
||||
- **Handsontable 内置右键菜单与自定义菜单互斥**
|
||||
|
||||
## 组件集成
|
||||
|
||||
### 1. db-hst 组件(Handsontable)
|
||||
- 使用自定义右键菜单组件 `contextmenu.vue`(空白区域右键)
|
||||
- 使用 Handsontable 内置 `contextMenu`(单元格右键)
|
||||
- 两种菜单互斥,打开一个会自动关闭另一个
|
||||
|
||||
### 2. db-tree 组件(Element Plus Tree)
|
||||
使用 Element Plus Dropdown 实现的右键菜单
|
||||
|
||||
## 工作原理
|
||||
|
||||
### contextMenuManager(全局菜单管理器)
|
||||
- 维护所有活动菜单的关闭回调函数
|
||||
- 当新菜单打开时,通知管理器关闭其他菜单
|
||||
- 组件卸载时自动取消注册
|
||||
|
||||
### 使用流程
|
||||
1. 组件挂载时,向管理器注册关闭回调
|
||||
2. 打开菜单前,调用 `contextMenuManager.notifyOpening()`
|
||||
3. 管理器关闭其他菜单,保留当前菜单
|
||||
4. 组件卸载时,自动取消注册
|
||||
|
||||
### db-hst 组件特殊处理
|
||||
- 在 `afterOnCellContextMenu` 钩子中关闭自定义菜单
|
||||
- 确保 Handsontable 内置菜单打开时,自定义菜单被关闭
|
||||
|
||||
## 示例代码
|
||||
|
||||
### 在组件中使用
|
||||
```typescript
|
||||
// 在组件中使用
|
||||
import { contextMenuManager } from './contextMenuManager'
|
||||
|
||||
// 注册菜单
|
||||
const unregister = contextMenuManager.register(hideContextMenu)
|
||||
|
||||
// 打开菜单时
|
||||
const showMenu = () => {
|
||||
contextMenuManager.notifyOpening(hideContextMenu)
|
||||
// 显示菜单逻辑
|
||||
}
|
||||
|
||||
// 组件卸载时
|
||||
onUnmounted(() => {
|
||||
unregister()
|
||||
})
|
||||
```
|
||||
|
||||
### Handsontable 配置
|
||||
```typescript
|
||||
const settings = {
|
||||
// ... 其他配置
|
||||
afterOnCellContextMenu: () => {
|
||||
// 关闭自定义菜单
|
||||
contextMenuRef.value?.hideContextMenu?.()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
- 确保每个菜单组件都正确注册和取消注册
|
||||
- 打开菜单前必须调用 `notifyOpening()`
|
||||
- 关闭回调函数应该是幂等的(可以多次调用)
|
||||
- Handsontable 的 `contextMenu` 配置与自定义菜单会自动互斥
|
||||
41
apps/web-ele/src/components/db-hst/base.ts
Normal file
41
apps/web-ele/src/components/db-hst/base.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {ref } from 'vue'
|
||||
// 边框样式配置
|
||||
export const borderStyle = {
|
||||
width: 2,
|
||||
color: '#0000ff',
|
||||
style: 'solid'
|
||||
}
|
||||
|
||||
// 构建当前选中行的边框配置
|
||||
export const selectCellBorderStyle = (_this:any, row: any) => {
|
||||
const hot = _this as any
|
||||
// 构建当前选中行的边框配置
|
||||
if (row < 0) return
|
||||
const colCount = hot.countCols()
|
||||
if (colCount <= 0) return
|
||||
|
||||
const customBorders = hot.getPlugin('customBorders')
|
||||
customBorders.clearBorders()
|
||||
customBorders.setBorders([[row, 0, row, colCount - 1]], { top: borderStyle, bottom: borderStyle })
|
||||
customBorders.setBorders([[row, 0, row, 0]], { start: borderStyle })
|
||||
customBorders.setBorders([[row, colCount - 1, row, colCount - 1]], { end: borderStyle })
|
||||
}
|
||||
|
||||
// 构建当前选中行的背景色配置
|
||||
export const selectedVisualRowRange = ref<{ from: number; to: number } | null>(null)
|
||||
|
||||
export const setSelectedVisualRows = (row: number, row2: number) => {
|
||||
if (row < 0 || row2 < 0) {
|
||||
selectedVisualRowRange.value = null
|
||||
return
|
||||
}
|
||||
const from = Math.min(row, row2)
|
||||
const to = Math.max(row, row2)
|
||||
selectedVisualRowRange.value = { from, to }
|
||||
}
|
||||
|
||||
export const isVisualRowSelected = (visualRowIndex: number) => {
|
||||
const range = selectedVisualRowRange.value
|
||||
if (!range) return false
|
||||
return visualRowIndex >= range.from && visualRowIndex <= range.to
|
||||
}
|
||||
56
apps/web-ele/src/components/db-hst/contextMenuManager.ts
Normal file
56
apps/web-ele/src/components/db-hst/contextMenuManager.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 全局右键菜单管理器
|
||||
* 用于管理多个右键菜单实例,确保同一时间只有一个菜单处于打开状态
|
||||
*/
|
||||
|
||||
type MenuCloseCallback = () => void
|
||||
|
||||
class ContextMenuManager {
|
||||
private activeMenus: Set<MenuCloseCallback> = new Set()
|
||||
|
||||
/**
|
||||
* 注册一个菜单实例
|
||||
* @param closeCallback 关闭菜单的回调函数
|
||||
* @returns 取消注册的函数
|
||||
*/
|
||||
register(closeCallback: MenuCloseCallback): () => void {
|
||||
this.activeMenus.add(closeCallback)
|
||||
|
||||
// 返回取消注册的函数
|
||||
return () => {
|
||||
this.activeMenus.delete(closeCallback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有已注册的菜单
|
||||
*/
|
||||
closeAll(): void {
|
||||
this.activeMenus.forEach(callback => {
|
||||
try {
|
||||
callback()
|
||||
} catch (error) {
|
||||
console.error('关闭菜单时出错:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知即将打开新菜单,关闭其他所有菜单
|
||||
* @param currentCloseCallback 当前菜单的关闭回调(不会被关闭)
|
||||
*/
|
||||
notifyOpening(currentCloseCallback: MenuCloseCallback): void {
|
||||
this.activeMenus.forEach(callback => {
|
||||
if (callback !== currentCloseCallback) {
|
||||
try {
|
||||
callback()
|
||||
} catch (error) {
|
||||
console.error('关闭其他菜单时出错:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const contextMenuManager = new ContextMenuManager()
|
||||
228
apps/web-ele/src/components/db-hst/contextmenu.vue
Normal file
228
apps/web-ele/src/components/db-hst/contextmenu.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, computed } from 'vue'
|
||||
import { contextMenuManager } from './contextMenuManager'
|
||||
|
||||
// 接收父组件传递的 props
|
||||
const props = defineProps<{
|
||||
hotTableComponent?: any
|
||||
menuItems?: Array<{
|
||||
key: string
|
||||
name: string
|
||||
callback?: (hotInstance: any) => void
|
||||
separator?: boolean
|
||||
}>
|
||||
}>()
|
||||
|
||||
// 自定义右键菜单状态
|
||||
const contextMenu = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
row: -1, // 记录右键点击的行索引
|
||||
col: -1 // 记录右键点击的列索引
|
||||
})
|
||||
|
||||
// 默认菜单项
|
||||
const defaultMenuItems = [
|
||||
{
|
||||
key: 'addRow',
|
||||
name: '新增一行',
|
||||
callback: (hotInstance: any) => {
|
||||
const rowCount = hotInstance.countRows()
|
||||
hotInstance.alter('insert_row_below', rowCount - 1, 1)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 合并默认菜单和自定义菜单
|
||||
const menuItems = computed(() => {
|
||||
return props.menuItems && props.menuItems.length > 0
|
||||
? props.menuItems
|
||||
: defaultMenuItems
|
||||
})
|
||||
|
||||
// 获取 hotInstance
|
||||
const getHotInstance = () => {
|
||||
return props.hotTableComponent?.hotInstance
|
||||
}
|
||||
|
||||
|
||||
// 处理右键菜单
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
// 如果没有菜单项,不处理右键菜单
|
||||
if (!props.menuItems || props.menuItems.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
// 判断是否点击在空白区域(非单元格区域)
|
||||
// 检查是否点击在 td 或 th 元素上
|
||||
const isCell = target.closest('td') || target.closest('th')
|
||||
|
||||
if (!isCell) {
|
||||
// 点击在空白区域,显示自定义菜单
|
||||
event.preventDefault()
|
||||
|
||||
// 菜单尺寸(需要与 CSS 中的实际尺寸匹配)
|
||||
const menuWidth = 180
|
||||
const menuItemHeight = 32 // 单个菜单项高度(padding + font-size)
|
||||
const menuPadding = 8 // 菜单上下 padding
|
||||
const menuHeight = menuItems.value.length * menuItemHeight + menuPadding
|
||||
|
||||
// 获取容器元素(Handsontable 的容器)
|
||||
const container = target.closest('.handsontable') as HTMLElement || target.closest('[class*="hot-"]') as HTMLElement
|
||||
|
||||
// 如果找到容器,使用容器边界;否则使用视口边界
|
||||
let viewportWidth = window.innerWidth
|
||||
let viewportHeight = window.innerHeight
|
||||
let offsetX = 0
|
||||
let offsetY = 0
|
||||
|
||||
if (container) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
viewportWidth = rect.right
|
||||
viewportHeight = rect.bottom
|
||||
offsetX = rect.left
|
||||
offsetY = rect.top
|
||||
}
|
||||
|
||||
// 计算菜单位置,确保不超出边界
|
||||
let x = event.clientX
|
||||
let y = event.clientY
|
||||
|
||||
// 如果右侧空间不足,向左显示
|
||||
if (x + menuWidth > viewportWidth) {
|
||||
x = Math.max(offsetX + 10, viewportWidth - menuWidth - 10)
|
||||
}
|
||||
|
||||
// 如果底部空间不足,向上显示
|
||||
if (y + menuHeight > viewportHeight) {
|
||||
y = Math.max(offsetY + 10, viewportHeight - menuHeight - 10)
|
||||
}
|
||||
|
||||
// 确保不会超出左边界和上边界
|
||||
x = Math.max(offsetX + 10, x)
|
||||
y = Math.max(offsetY + 10, y)
|
||||
|
||||
// 使用统一的显示方法
|
||||
showContextMenu(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏右键菜单
|
||||
const hideContextMenu = () => {
|
||||
contextMenu.visible = false
|
||||
}
|
||||
|
||||
// 显示右键菜单(在打开前先关闭其他菜单)
|
||||
const showContextMenu = (x: number, y: number) => {
|
||||
// 通知管理器即将打开新菜单,关闭其他菜单
|
||||
contextMenuManager.notifyOpening(hideContextMenu)
|
||||
|
||||
contextMenu.visible = true
|
||||
contextMenu.x = x
|
||||
contextMenu.y = y
|
||||
}
|
||||
|
||||
// 菜单项点击处理
|
||||
const handleMenuAction = (item: any) => {
|
||||
console.log('菜单操作:', item.key)
|
||||
|
||||
const hotInstance = getHotInstance()
|
||||
if (!hotInstance) {
|
||||
console.warn('hotInstance 未初始化')
|
||||
hideContextMenu()
|
||||
return
|
||||
}
|
||||
|
||||
// 执行自定义回调
|
||||
if (item.callback && typeof item.callback === 'function') {
|
||||
item.callback(hotInstance)
|
||||
}
|
||||
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
handleContextMenu,
|
||||
hideContextMenu
|
||||
})
|
||||
|
||||
// 取消注册函数
|
||||
let unregister: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// 注册到全局菜单管理器
|
||||
unregister = contextMenuManager.register(hideContextMenu)
|
||||
|
||||
// 监听全局点击事件,隐藏菜单
|
||||
document.addEventListener('click', hideContextMenu)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 取消注册
|
||||
if (unregister) {
|
||||
unregister()
|
||||
}
|
||||
|
||||
document.removeEventListener('click', hideContextMenu)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 自定义右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="custom-context-menu"
|
||||
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||
@click.stop
|
||||
>
|
||||
<template v-for="(item, index) in menuItems" :key="item.key || index">
|
||||
<!-- 分隔线 -->
|
||||
<div v-if="'separator' in item && item.separator" class="menu-separator"></div>
|
||||
<!-- 菜单项 -->
|
||||
<div v-else class="menu-item" @click="handleMenuAction(item)">
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
/* 自定义右键菜单样式 */
|
||||
.custom-context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
min-width: 180px;
|
||||
padding: 4px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.menu-item span{
|
||||
font-size: 14px;
|
||||
margin-inline: calc(2* 4px);
|
||||
}
|
||||
.menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-separator {
|
||||
height: 1px;
|
||||
background-color: #e8e8e8;
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -13,22 +13,38 @@ const closeDropdown = () => {
|
||||
const openDropdown = (
|
||||
td: HTMLTableCellElement,
|
||||
value: unknown,
|
||||
source: string[],
|
||||
source: any[],
|
||||
onSelect: (opt: string) => void,
|
||||
isOptionDisabled?: (opt: string) => boolean,
|
||||
onAfterSelect?: (oldValue: unknown, newValue: string, optData?: any) => void,
|
||||
) => {
|
||||
closeDropdown()
|
||||
const menu = document.createElement('div')
|
||||
menu.className = 'ht-dropdown-menu'
|
||||
const frag = document.createDocumentFragment()
|
||||
|
||||
for (const opt of source) {
|
||||
// 支持对象格式 {value, label} 和字符串格式
|
||||
const optValue = typeof opt === 'object' && opt !== null ? opt.value : opt
|
||||
const optLabel = typeof opt === 'object' && opt !== null ? opt.label : opt
|
||||
|
||||
const item = document.createElement('div')
|
||||
item.className = 'ht-dropdown-item'
|
||||
item.textContent = opt
|
||||
if (String(value) === String(opt)) item.classList.add('is-selected')
|
||||
const disabled = isDisabled.value && isOptionDisabled?.(opt) === true
|
||||
item.textContent = optLabel ?? ''
|
||||
if (String(value) === String(optValue)) item.classList.add('is-selected')
|
||||
const disabled = isDisabled.value && isOptionDisabled?.(String(optValue)) === true
|
||||
if (disabled) { item.classList.add('is-disabled'); item.setAttribute('aria-disabled', 'true') }
|
||||
item.onclick = (e) => { e.stopPropagation(); if (disabled) return; onSelect(opt); closeDropdown() }
|
||||
item.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (disabled) return;
|
||||
onSelect(String(optValue));
|
||||
// 调用选择后的回调,传递旧值、新值和完整数据
|
||||
if (onAfterSelect) {
|
||||
const optData = typeof opt === 'object' && opt !== null ? opt.data : undefined
|
||||
onAfterSelect(value, String(optValue), optData)
|
||||
}
|
||||
closeDropdown()
|
||||
}
|
||||
frag.appendChild(item)
|
||||
}
|
||||
menu.appendChild(frag)
|
||||
@@ -51,23 +67,71 @@ const openDropdown = (
|
||||
currentOnDocClick = (ev: MouseEvent) => { const target = ev.target as Node; if (currentDropdownEl && !currentDropdownEl.contains(target)) closeDropdown() }
|
||||
document.addEventListener('click', currentOnDocClick, true)
|
||||
}
|
||||
|
||||
export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: any) {
|
||||
td.innerHTML = ''
|
||||
|
||||
// 检查是否需要设置验证背景色
|
||||
const cellMeta = instance.getCellMeta(row, column)
|
||||
const isValid = cellMeta?.valid !== false
|
||||
|
||||
// 如果单元格被标记为无效,设置红色背景
|
||||
if (!isValid) {
|
||||
td.style.backgroundColor = '#ffbeba' // 淡红色背景
|
||||
} else {
|
||||
td.style.backgroundColor = '' // 清除背景色
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'ht-cell-dropdown'
|
||||
const valueEl = document.createElement('span')
|
||||
valueEl.className = 'ht-cell-value'
|
||||
valueEl.textContent = value ?? ''
|
||||
|
||||
// 基于列配置启用"唯一选择"禁用逻辑
|
||||
isDisabled.value = Boolean(cellProperties?.isOnlySelect)
|
||||
|
||||
//TODO 暂时性,后面要删除
|
||||
// 如果 isOnlySelect 为 true,设置当前单元格为只读
|
||||
const isReadOnly = isDisabled.value && value !== null && value !== undefined && String(value).trim() !== ''
|
||||
if (isReadOnly) {
|
||||
instance.setCellMeta(row, column, 'readOnly', true)
|
||||
// 添加只读样式
|
||||
wrapper.classList.add('is-readonly')
|
||||
td.style.cursor = 'not-allowed'
|
||||
td.style.opacity = '0.6'
|
||||
} else {
|
||||
instance.setCellMeta(row, column, 'readOnly', false)
|
||||
td.style.cursor = 'pointer'
|
||||
td.style.opacity = '1'
|
||||
}
|
||||
|
||||
const source: any[] = Array.isArray(cellProperties?.source)
|
||||
? cellProperties.source
|
||||
: Array.isArray(cellProperties?.customDropdownSource)
|
||||
? cellProperties.customDropdownSource
|
||||
: []
|
||||
|
||||
// 根据 value 查找对应的 label 显示
|
||||
let displayText = value ?? ''
|
||||
if (value !== null && value !== undefined) {
|
||||
const matchedOption = source.find(opt => {
|
||||
const optValue = typeof opt === 'object' && opt !== null ? opt.value : opt
|
||||
return String(optValue) === String(value)
|
||||
})
|
||||
if (matchedOption) {
|
||||
displayText = typeof matchedOption === 'object' && matchedOption !== null
|
||||
? matchedOption.label
|
||||
: matchedOption
|
||||
}
|
||||
}
|
||||
|
||||
valueEl.textContent = displayText
|
||||
const caretEl = document.createElement('span')
|
||||
caretEl.className = 'ht-cell-caret'
|
||||
wrapper.appendChild(valueEl)
|
||||
wrapper.appendChild(caretEl)
|
||||
td.appendChild(wrapper)
|
||||
const source: string[] = Array.isArray(cellProperties?.source)
|
||||
? cellProperties.source
|
||||
: Array.isArray(cellProperties?.customDropdownSource)
|
||||
? cellProperties.customDropdownSource
|
||||
: []
|
||||
|
||||
let disabledSet = new Set<string>()
|
||||
if (isDisabled.value) {
|
||||
const colValues = instance.getSourceDataAtCol(column) as unknown[]
|
||||
@@ -75,6 +139,32 @@ export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement,
|
||||
disabledSet = new Set((Array.isArray(colValues) ? colValues : []).map(v => String(v)))
|
||||
disabledSet.delete(currentStr)
|
||||
}
|
||||
wrapper.onclick = (e) => { e.stopPropagation(); openDropdown(td, value, source, (opt) => instance.setDataAtCell(row, column, opt), (opt) => isDisabled.value && disabledSet.has(String(opt))) }
|
||||
|
||||
//在 dropdown 的点击事件中阻止冒泡(推荐)
|
||||
wrapper.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
//TODO 暂时性,后面要删除
|
||||
// 如果是只读状态,不打开下拉框
|
||||
if (isReadOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
const onAfterSelect = cellProperties?.onAfterSelect
|
||||
openDropdown(
|
||||
td,
|
||||
value,
|
||||
source,
|
||||
(opt) => instance.setDataAtCell(row, column, opt),
|
||||
(opt) => isDisabled.value && disabledSet.has(String(opt)),
|
||||
onAfterSelect ? (oldValue, newValue, optData) => onAfterSelect(instance, row, column, oldValue, newValue, optData) : undefined
|
||||
)
|
||||
}
|
||||
|
||||
// 阻止 mousedown 事件冒泡,防止触发 beforeOnCellMouseDown
|
||||
wrapper.onmousedown = (e) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
return td
|
||||
}
|
||||
@@ -6,16 +6,28 @@ import { HotTable } from '@handsontable/vue3'
|
||||
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import 'handsontable/styles/handsontable.css'
|
||||
import 'handsontable/styles/ht-theme-main.css'
|
||||
// import 'handsontable/styles/ht-theme-main.css'
|
||||
import 'handsontable/styles/ht-theme-classic.css';
|
||||
registerAllModules()
|
||||
registerLanguageDictionary(zhCN)
|
||||
import { handlerDropdownRenderer } from './dropdown'
|
||||
import { handlerTableRenderer } from './table'
|
||||
import { handlerDuplicateCodeRenderer } from './text'
|
||||
import { computeCodeColWidth,codeRenderer } from './tree'
|
||||
import ContextMenu from './contextmenu.vue'
|
||||
import { handleRowOperation } from '#/components/db-hst/tree'
|
||||
// import { sourceDataObject } from './mockData'
|
||||
// const language = ref('zh-CN')
|
||||
defineOptions({ name: 'DbHst' });
|
||||
const componentProps = defineProps<{ settings?: any }>()
|
||||
const componentProps = defineProps<{
|
||||
settings?: any
|
||||
contextMenuItems?: Array<{
|
||||
key: string
|
||||
name: string
|
||||
callback?: (hotInstance: any) => void
|
||||
separator?: boolean
|
||||
}>
|
||||
}>()
|
||||
// 导入和注册插件和单元格类型
|
||||
// import { registerCellType, NumericCellType } from 'handsontable/cellTypes';
|
||||
// import { registerPlugin, UndoRedo } from 'handsontable/plugins';
|
||||
@@ -24,13 +36,15 @@ const componentProps = defineProps<{ settings?: any }>()
|
||||
// const tableHeight = computed(() => componentProps.height ?? 0)
|
||||
// const defaultData = ref<any[][]>(Array.from({ length: 30 }, () => Array(componentProps.columns?.length ?? 0).fill('')))
|
||||
const hotTableComponent = ref<any>(null)
|
||||
|
||||
const selectedRow = ref<number | null>(null) // 记录当前选中的行
|
||||
const codeColWidth = ref<number>(120)
|
||||
|
||||
|
||||
// const colHeaders = ref<string[]>([])
|
||||
let defaultSettings = {
|
||||
themeName: 'ht-theme-main',
|
||||
// themeName: 'ht-theme-main',
|
||||
themeName: 'ht-theme-classic',
|
||||
|
||||
language: 'zh-CN',
|
||||
// data: sourceDataObject,
|
||||
// colWidths: [100, 120, 100, 100, 100, 100],
|
||||
@@ -40,7 +54,7 @@ let defaultSettings = {
|
||||
// return (index + 1) * 40;
|
||||
// },
|
||||
// colWidths: undefined,
|
||||
rowHeights: '23px', // 固定行高
|
||||
rowHeights: 23, // 固定行高
|
||||
wordWrap: false,// 禁止单元格内容自动换行
|
||||
|
||||
//manualColumnMove: true,
|
||||
@@ -90,25 +104,69 @@ let defaultSettings = {
|
||||
|
||||
// return cellProperties;
|
||||
// },
|
||||
modifyColWidth: (width: number, col: number) => {
|
||||
const hot = hotInstance.value
|
||||
if (!hot) return width
|
||||
const codeCol = hot.propToCol('code')
|
||||
// console.log('modifyColWidth',codeCol,width)
|
||||
return col === codeCol ? (codeColWidth.value ?? width) : width
|
||||
},
|
||||
afterChange: (changes: any, source: string) => {
|
||||
if (!changes || !hotInstance.value) return
|
||||
if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return
|
||||
const hot = hotInstance.value
|
||||
const codeCol = hot.propToCol('code')
|
||||
const hasCodeEdit = changes.some((c: any) => c && (c[1] === 'code' || c[1] === codeCol))
|
||||
// console.log('afterChange',changes,hasCodeEdit, codeCol)
|
||||
if (!hasCodeEdit) return
|
||||
codeColWidth.value = computeCodeColWidth(hot)
|
||||
// console.log('afterChange',codeColWidth.value)
|
||||
hot.render()
|
||||
// console.log('afterChange',codeColWidth.value)
|
||||
// afterSelection(row1: number, _col1: number, _row2: number, _col2: number) {
|
||||
// const hot = this as any
|
||||
// if (selectedRow.value !== null && selectedRow.value !== row1) {
|
||||
// const colCount = hot.countCols()
|
||||
// for (let c = 0; c < colCount; c++) {
|
||||
// const meta = hot.getCellMeta(selectedRow.value, c)
|
||||
// const classes = (meta.className || '').split(' ').filter(Boolean)
|
||||
// const idx = classes.indexOf('row-highlight')
|
||||
// if (idx !== -1) classes.splice(idx, 1)
|
||||
// hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' '))
|
||||
// }
|
||||
// }
|
||||
|
||||
// selectedRow.value = row1
|
||||
|
||||
// const colCount = hot.countCols()
|
||||
// for (let c = 0; c < colCount; c++) {
|
||||
// const meta = hot.getCellMeta(row1, c)
|
||||
// const classes = (meta.className || '').split(' ').filter(Boolean)
|
||||
// if (!classes.includes('row-highlight')) classes.push('row-highlight')
|
||||
// hot.setCellMeta(row1, c, 'className', classes.join(' '))
|
||||
// }
|
||||
|
||||
// hot.render()
|
||||
// },
|
||||
//afterDeselect() {
|
||||
// const hot = this as any
|
||||
// if (selectedRow.value === null) return
|
||||
// const colCount = hot.countCols()
|
||||
// for (let c = 0; c < colCount; c++) {
|
||||
// const meta = hot.getCellMeta(selectedRow.value, c)
|
||||
// const classes = (meta.className || '').split(' ').filter(Boolean)
|
||||
// const idx = classes.indexOf('row-highlight')
|
||||
// if (idx !== -1) classes.splice(idx, 1)
|
||||
// hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' '))
|
||||
// }
|
||||
// selectedRow.value = null
|
||||
// hot.render()
|
||||
//},
|
||||
|
||||
// modifyColWidth(width: number, col: number) {
|
||||
// const hot = hotInstance.value
|
||||
// if (!hot) return width
|
||||
// const codeCol = hot.propToCol('code')
|
||||
// // console.log('modifyColWidth',codeCol,width)
|
||||
// return col === codeCol ? (codeColWidth.value ?? width) : width
|
||||
// },
|
||||
// afterChange(changes: any, source: string) {
|
||||
// if (!changes || !hotInstance.value) return
|
||||
// if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return
|
||||
// const hot = hotInstance.value
|
||||
// const codeCol = hot.propToCol('code')
|
||||
// const hasCodeEdit = changes.some((c: any) => c && (c[1] === 'code' || c[1] === codeCol))
|
||||
// // console.log('afterChange',changes,hasCodeEdit, codeCol)
|
||||
// if (!hasCodeEdit) return
|
||||
// codeColWidth.value = computeCodeColWidth(hot)
|
||||
// // console.log('afterChange',codeColWidth.value)
|
||||
// hot.render()
|
||||
// // console.log('afterChange',codeColWidth.value)
|
||||
// },
|
||||
afterOnCellContextMenu: () => {
|
||||
// Handsontable 内置右键菜单打开后,关闭自定义菜单
|
||||
contextMenuRef.value?.hideContextMenu?.()
|
||||
},
|
||||
}
|
||||
// 合并外部 settings 和默认配置
|
||||
@@ -116,6 +174,12 @@ let hotSettings = {}
|
||||
// 保留必要的回调函数
|
||||
|
||||
const hotInstance = ref<any>(null)
|
||||
const contextMenuRef = ref<any>(null)
|
||||
|
||||
// 处理右键菜单事件
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
contextMenuRef.value?.handleContextMenu(event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
hotInstance.value = hotTableComponent.value?.hotInstance
|
||||
@@ -127,15 +191,17 @@ watch(
|
||||
() => componentProps.settings,
|
||||
(newSettings) => {
|
||||
if (!newSettings) return
|
||||
|
||||
const merged = {
|
||||
...defaultSettings,
|
||||
...newSettings,
|
||||
}
|
||||
Object.assign(hotSettings, merged)
|
||||
hotSettings = merged
|
||||
|
||||
// console.log(merged)
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true,deep:true }
|
||||
)
|
||||
|
||||
const loadData = (rows: any[][]) => {
|
||||
@@ -143,12 +209,51 @@ const loadData = (rows: any[][]) => {
|
||||
if (!hotInstance.value) return
|
||||
// hotInstance.value.loadData(rows.length === 0?defaultData.value:rows)
|
||||
hotInstance.value.loadData(rows)
|
||||
console.log('Source Data:', hotInstance.value.getSourceData());
|
||||
//console.log('Source Data:', hotInstance.value.getSourceData());
|
||||
}
|
||||
|
||||
const updateCodeColWidth = () => {
|
||||
if (!hotInstance.value) return
|
||||
codeColWidth.value = computeCodeColWidth(hotInstance.value)
|
||||
const newWidth = computeCodeColWidth(hotInstance.value)
|
||||
|
||||
// 如果宽度没有变化,不需要更新
|
||||
if (newWidth === codeColWidth.value) return
|
||||
|
||||
codeColWidth.value = newWidth
|
||||
|
||||
// 查找配置了 code: true 的列
|
||||
const currentSettings = hotInstance.value.getSettings()
|
||||
const columns = currentSettings.columns || []
|
||||
const codeColIndex = columns.findIndex((col: any) => col.code === true)
|
||||
|
||||
if (codeColIndex !== null && codeColIndex >= 0) {
|
||||
// 获取当前列数
|
||||
const colCount = hotInstance.value.countCols()
|
||||
const currentColWidths = currentSettings.colWidths
|
||||
|
||||
// 构建新的列宽数组
|
||||
const newColWidths: number[] = []
|
||||
for (let i = 0; i < colCount; i++) {
|
||||
if (i === codeColIndex) {
|
||||
newColWidths[i] = codeColWidth.value
|
||||
} else {
|
||||
// 获取其他列的当前宽度
|
||||
if (Array.isArray(currentColWidths)) {
|
||||
newColWidths[i] = currentColWidths[i] || 100
|
||||
} else if (typeof currentColWidths === 'function') {
|
||||
newColWidths[i] = 100
|
||||
} else {
|
||||
newColWidths[i] = currentColWidths || 100
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(newColWidths)
|
||||
// 更新列宽
|
||||
hotInstance.value.updateSettings({
|
||||
colWidths: newColWidths
|
||||
})
|
||||
}
|
||||
|
||||
hotInstance.value.render()
|
||||
}
|
||||
defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, codeColWidth })
|
||||
@@ -156,11 +261,15 @@ defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, cod
|
||||
|
||||
Handsontable.renderers.registerRenderer("db-table", handlerTableRenderer);
|
||||
Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
||||
|
||||
Handsontable.renderers.registerRenderer("db-duplicate", handlerDuplicateCodeRenderer);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<hot-table ref="hotTableComponent" :settings="hotSettings"></hot-table>
|
||||
<div @contextmenu="handleContextMenu">
|
||||
<hot-table ref="hotTableComponent" :settings="hotSettings"></hot-table>
|
||||
<ContextMenu ref="contextMenuRef" :hotTableComponent="hotTableComponent" :menuItems="componentProps.contextMenuItems" />
|
||||
</div>
|
||||
|
||||
<!-- <div id="hot-dialog-container" style="display:none">
|
||||
<div class="ht-dialog-content">
|
||||
<h3>执行操作</h3>
|
||||
@@ -214,14 +323,14 @@ Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
||||
.ht_master .wtHolder::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
} */
|
||||
|
||||
/* 滚动条width */
|
||||
.ht_master .wtHolder{
|
||||
/* overflow: hidden !important; */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #a6a8ac #ecf0f1;
|
||||
}
|
||||
/* dropdown */
|
||||
.ht-cell-dropdown { display: flex; align-items: center; justify-content: space-between; width: 100%; position: relative; box-sizing: border-box; height: 100%; }
|
||||
.ht-cell-dropdown { display: flex; align-items: center; justify-content: center; width: 100%; position: relative; box-sizing: border-box; height: 100%; }
|
||||
.ht-cell-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ht-cell-value:empty::after { content: "\200b"; }
|
||||
.ht-cell-caret { position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #979797; }
|
||||
@@ -283,66 +392,86 @@ Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
||||
.hot-dropdown-search-input { width: 100%; padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px; outline: none; transition: border-color 0.2s; }
|
||||
.hot-dropdown-search-input:focus { border-color: #3b82f6; }
|
||||
.hot-dropdown-table-wrapper { overflow: auto; flex: 1; }
|
||||
.hot-dropdown-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.hot-dropdown-table thead th { position: sticky; top: 0; background: #f9fafb; font-weight: 600; color: #374151; padding: 8px; border-bottom: 1px solid #e5e7eb; text-align: left; }
|
||||
.hot-dropdown-table { width: 100%; border-collapse: collapse; font-size: 12px; text-align: center; }
|
||||
.hot-dropdown-table thead th { position: sticky; top: 0; background: #f9fafb; font-weight: 600; color: #374151; padding: 8px; border-bottom: 1px solid #e5e7eb; text-align: center; }
|
||||
.hot-dropdown-table tbody td { padding: 8px; border-bottom: 1px solid #f3f4f6; color: #374151; }
|
||||
.hot-dropdown-row { cursor: pointer; }
|
||||
.hot-dropdown-row:hover { background: #f3f4f6; }
|
||||
|
||||
/** 指引线line */
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 1px;
|
||||
order: -2;
|
||||
|
||||
/* 整行高亮样式 */
|
||||
.row-highlight {
|
||||
background-color: #e9ecfc !important; /* 浅蓝色背景 */
|
||||
}
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:last-child {
|
||||
padding-left: calc(var(--ht-icon-size) + 5px);
|
||||
|
||||
/* 确保 Handsontable 右键菜单在 ElDialog 之上 - 必须是全局样式 */
|
||||
.handsontable.htDropdownMenu:not(.htGhostTable),
|
||||
.handsontable.htContextMenu:not(.htGhostTable),
|
||||
.handsontable.htFiltersConditionsMenu:not(.htGhostTable) {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty::before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -13px;
|
||||
height: 26px;
|
||||
width: 1px;
|
||||
border-left: 1px solid #ababab;
|
||||
|
||||
.ht-id-cell {
|
||||
position: relative !important;
|
||||
z-index: 3 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty::after{
|
||||
content: '';
|
||||
|
||||
.ht-id-icon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
border-top: 1px solid #ababab;
|
||||
}
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty{
|
||||
padding-left: 7px;
|
||||
right: -14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* 最后一个 ht_nestingLevel_empty(rowHeader 前面的那个) */
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty + .rowHeader {
|
||||
/* 通过相邻选择器反向选择 */
|
||||
padding-left: 10px !important
|
||||
}
|
||||
/* 选择后面还有 ht_nestingLevel_empty 的元素(不是最后一个) */
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:has(+ .ht_nestingLevel_empty)::before {
|
||||
/* 你的样式 */
|
||||
/* height: 0px; */
|
||||
}
|
||||
/* 选择后面还有 ht_nestingLevel_empty 的元素(不是最后一个) */
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:has(+ .ht_nestingLevel_empty)::after {
|
||||
/* 你的样式 */
|
||||
width: 0px !important;
|
||||
.ht-id-cell.current .ht-id-icon,
|
||||
.ht-id-cell.area .ht-id-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* 或者用这个:选择后面不是 ht_nestingLevel_empty 的那个 */
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:not(:has(+ .ht_nestingLevel_empty))::before {
|
||||
/* height: 13px; */
|
||||
.handsontable {
|
||||
--ht-tree-line-color: #7c7c7c;
|
||||
--ht-tree-line-style: solid;
|
||||
--ht-tree-line-width: 1px;
|
||||
--ht-tree-indent: 14px;
|
||||
}
|
||||
/** 新树形连接线 */
|
||||
.handsontable .ht_treeCell {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:not(:has(+ .ht_nestingLevel_empty))::after {
|
||||
/* 你的特殊样式 */
|
||||
|
||||
.handsontable .ht_treeIndentLayer {
|
||||
flex: 0 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handsontable .ht_treeContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.handsontable .ht_treeToggleSpacer {
|
||||
display: inline-block;
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.handsontable .ht_treeContent {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 整行高亮样式 */
|
||||
.handsontable td.ht_rowHighlight:not(.current):not(.area) {
|
||||
background-color: #e9ecfc;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
490
apps/web-ele/src/components/db-hst/nestedRows.ts
Normal file
490
apps/web-ele/src/components/db-hst/nestedRows.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
export type TreeNode = Record<string, unknown> & {
|
||||
__id?: string
|
||||
__children?: TreeNode[]
|
||||
level?: string | null
|
||||
}
|
||||
|
||||
type FlatRowMeta = {
|
||||
id: string
|
||||
hasChildren: boolean
|
||||
lineKey: string
|
||||
path: string
|
||||
node: TreeNode
|
||||
siblings: TreeNode[]
|
||||
indexWithinParent: number
|
||||
depth: number
|
||||
}
|
||||
|
||||
type FlatTreeIndex = {
|
||||
root: TreeNode[]
|
||||
rows: TreeNode[]
|
||||
metaByRow: FlatRowMeta[]
|
||||
}
|
||||
|
||||
type TreeLinePaint = {
|
||||
key: string
|
||||
width: string
|
||||
backgroundImage: string
|
||||
backgroundSize: string
|
||||
backgroundPosition: string
|
||||
}
|
||||
|
||||
type UseParentChildLineOptions = {
|
||||
getHotInstance: () => any
|
||||
}
|
||||
|
||||
export const useParentChildLineNestedRowsFalse = (options: UseParentChildLineOptions) => {
|
||||
const treeLinePaintCache = new Map<string, TreeLinePaint>()
|
||||
const collapsedNodeIds = ref(new Set<string>())
|
||||
const sourceTreeData = ref<TreeNode[]>([])
|
||||
const flatIndex = ref<FlatTreeIndex | null>(null)
|
||||
|
||||
let nextNodeId = 1
|
||||
|
||||
const maybeBumpNextNodeId = (id: string) => {
|
||||
if (!/^\d+$/.test(id)) return
|
||||
const num = Number(id)
|
||||
if (!Number.isSafeInteger(num)) return
|
||||
if (num >= nextNodeId) nextNodeId = num + 1
|
||||
}
|
||||
|
||||
const getNodeId = (node: TreeNode) => {
|
||||
if (node.__id) {
|
||||
maybeBumpNextNodeId(node.__id)
|
||||
return node.__id
|
||||
}
|
||||
const id = String(nextNodeId++)
|
||||
node.__id = id
|
||||
return id
|
||||
}
|
||||
|
||||
const normalizeNode = (node: TreeNode): TreeNode => {
|
||||
getNodeId(node)
|
||||
if (!Array.isArray(node.__children)) node.__children = []
|
||||
return node
|
||||
}
|
||||
|
||||
const buildFlatTreeIndex = (root: TreeNode[], collapsedIds: Set<string>): FlatTreeIndex => {
|
||||
const rows: TreeNode[] = []
|
||||
const metaByRow: FlatRowMeta[] = []
|
||||
|
||||
const visit = (list: TreeNode[], depth: number, flagsStr: string, basePath: string) => {
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const node = normalizeNode(list[index])
|
||||
const id = getNodeId(node)
|
||||
|
||||
const isLast = index === list.length - 1
|
||||
const children = node.__children ?? []
|
||||
const hasChildren = children.length > 0
|
||||
const path = depth === 0 ? String(index) : `${basePath}-${index + 1}`
|
||||
const lineKey = depth <= 0 ? '0' : `${depth}|${isLast ? 1 : 0}|${flagsStr}`
|
||||
|
||||
rows.push(node)
|
||||
metaByRow.push({
|
||||
id,
|
||||
depth,
|
||||
hasChildren,
|
||||
lineKey,
|
||||
path,
|
||||
node,
|
||||
siblings: list,
|
||||
indexWithinParent: index,
|
||||
})
|
||||
|
||||
if (hasChildren && !collapsedIds.has(id)) {
|
||||
const nextFlagsStr = depth >= 1 ? `${flagsStr}${isLast ? '0' : '1'}` : flagsStr
|
||||
visit(children, depth + 1, nextFlagsStr, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(root, 0, '', '')
|
||||
return { root, rows, metaByRow }
|
||||
}
|
||||
|
||||
const getTreeLinePaint = (key: string) => {
|
||||
const cached = treeLinePaintCache.get(key)
|
||||
if (cached) return cached
|
||||
|
||||
if (key === '0') {
|
||||
const empty = { key, width: '0px', backgroundImage: '', backgroundSize: '', backgroundPosition: '' }
|
||||
treeLinePaintCache.set(key, empty)
|
||||
return empty
|
||||
}
|
||||
|
||||
const firstSep = key.indexOf('|')
|
||||
const secondSep = key.indexOf('|', firstSep + 1)
|
||||
const level = Number(key.slice(0, firstSep))
|
||||
const isLast = key.slice(firstSep + 1, secondSep) === '1'
|
||||
const flagsStr = key.slice(secondSep + 1)
|
||||
|
||||
const color = 'var(--ht-tree-line-color)'
|
||||
const width = 'var(--ht-tree-line-width)'
|
||||
const indent = 'var(--ht-tree-indent)'
|
||||
|
||||
const images: string[] = []
|
||||
const sizes: string[] = []
|
||||
const positions: string[] = []
|
||||
|
||||
for (let i = 0; i < flagsStr.length; i++) {
|
||||
if (flagsStr.charCodeAt(i) !== 49) continue
|
||||
images.push(`linear-gradient(${color}, ${color})`)
|
||||
sizes.push(`${width} 100%`)
|
||||
positions.push(`calc(${indent} * ${i} + (${indent} / 2)) 0`)
|
||||
}
|
||||
|
||||
const selfDepth = level - 1
|
||||
images.push(`linear-gradient(${color}, ${color})`)
|
||||
sizes.push(`${width} ${isLast ? '50%' : '100%'}`)
|
||||
positions.push(`calc(${indent} * ${selfDepth} + (${indent} / 2)) 0`)
|
||||
|
||||
images.push(`linear-gradient(to right, ${color}, ${color})`)
|
||||
sizes.push(`calc(${indent} / 2) ${width}`)
|
||||
positions.push(`calc(${indent} * ${selfDepth} + (${indent} / 2)) 50%`)
|
||||
|
||||
const paint = {
|
||||
key,
|
||||
width: `calc(var(--ht-tree-indent) * ${level})`,
|
||||
backgroundImage: images.join(', '),
|
||||
backgroundSize: sizes.join(', '),
|
||||
backgroundPosition: positions.join(', '),
|
||||
}
|
||||
if (treeLinePaintCache.size > 5000) treeLinePaintCache.clear()
|
||||
treeLinePaintCache.set(key, paint)
|
||||
return paint
|
||||
}
|
||||
|
||||
const getTreeCellDom = (TD: HTMLTableCellElement) => {
|
||||
const currentTreeCell = TD.firstElementChild as HTMLElement | null
|
||||
if (currentTreeCell && currentTreeCell.classList.contains('ht_treeCell')) {
|
||||
const indentLayer = currentTreeCell.firstElementChild as HTMLElement
|
||||
const content = currentTreeCell.lastElementChild as HTMLElement
|
||||
return {
|
||||
treeCell: currentTreeCell,
|
||||
indentLayer,
|
||||
content,
|
||||
toggleEl: content.firstElementChild as HTMLElement,
|
||||
textEl: content.lastElementChild as HTMLElement,
|
||||
}
|
||||
}
|
||||
|
||||
const treeCell = document.createElement('div')
|
||||
treeCell.className = 'ht_treeCell'
|
||||
treeCell.style.pointerEvents = 'none'
|
||||
const indentLayer = document.createElement('span')
|
||||
indentLayer.className = 'ht_treeIndentLayer'
|
||||
// indentLayer 不需要交互,让事件穿透
|
||||
//indentLayer.style.pointerEvents = 'none'
|
||||
|
||||
const content = document.createElement('div')
|
||||
content.className = 'ht_treeContent'
|
||||
// content 需要响应点击(折叠/展开按钮),但允许右键菜单冒泡
|
||||
//content.style.pointerEvents = 'none'
|
||||
|
||||
const toggleSpacer = document.createElement('span')
|
||||
toggleSpacer.className = 'ht_treeToggleSpacer'
|
||||
const text = document.createElement('span')
|
||||
text.className = 'rowHeader'
|
||||
|
||||
content.appendChild(toggleSpacer)
|
||||
content.appendChild(text)
|
||||
treeCell.appendChild(indentLayer)
|
||||
treeCell.appendChild(content)
|
||||
TD.replaceChildren(treeCell)
|
||||
|
||||
return {
|
||||
treeCell,
|
||||
indentLayer,
|
||||
content,
|
||||
toggleEl: toggleSpacer,
|
||||
textEl: text,
|
||||
}
|
||||
}
|
||||
|
||||
const rebuildDataFromTree = (hotOverride?: any) => {
|
||||
const index = buildFlatTreeIndex(sourceTreeData.value, collapsedNodeIds.value)
|
||||
flatIndex.value = index
|
||||
const hot = hotOverride ?? options.getHotInstance()
|
||||
if (hot) hot.loadData(index.rows)
|
||||
}
|
||||
|
||||
const toggleNodeCollapsed = (nodeId: string) => {
|
||||
if (collapsedNodeIds.value.has(nodeId)) collapsedNodeIds.value.delete(nodeId)
|
||||
else collapsedNodeIds.value.add(nodeId)
|
||||
rebuildDataFromTree()
|
||||
}
|
||||
|
||||
const normalizeTreeData = (input: unknown): TreeNode[] => {
|
||||
const root = Array.isArray(input)
|
||||
? (input as TreeNode[])
|
||||
: input && typeof input === 'object'
|
||||
? [input as TreeNode]
|
||||
: []
|
||||
const stack: unknown[] = [...root]
|
||||
|
||||
while (stack.length) {
|
||||
const current = stack.pop()
|
||||
if (!current || typeof current !== 'object') continue
|
||||
const node = current as TreeNode
|
||||
|
||||
const maybeChildren = (node as any).__children
|
||||
if (!Array.isArray(maybeChildren)) {
|
||||
const altChildren = (node as any).children
|
||||
node.__children = Array.isArray(altChildren) ? (altChildren as TreeNode[]) : []
|
||||
} else {
|
||||
node.__children = maybeChildren as TreeNode[]
|
||||
}
|
||||
|
||||
for (let i = node.__children.length - 1; i >= 0; i--) {
|
||||
const child = node.__children[i]
|
||||
if (!child || typeof child !== 'object') node.__children.splice(i, 1)
|
||||
}
|
||||
|
||||
normalizeNode(node)
|
||||
for (let i = 0; i < node.__children.length; i++) stack.push(node.__children[i])
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
const load = (data: unknown) => {
|
||||
collapsedNodeIds.value.clear()
|
||||
treeLinePaintCache.clear()
|
||||
nextNodeId = 1
|
||||
sourceTreeData.value = normalizeTreeData(data)
|
||||
rebuildDataFromTree()
|
||||
}
|
||||
|
||||
const createNode = (dataSchema: any): TreeNode => {
|
||||
const node = {
|
||||
...dataSchema,
|
||||
__children: [],
|
||||
} as TreeNode
|
||||
normalizeNode(node)
|
||||
return node
|
||||
}
|
||||
|
||||
const getSelectedVisualRowRange = (hot: any): { startRow: number; amount: number } | null => {
|
||||
const sel = hot.getSelectedLast?.() ?? hot.getSelected?.()?.[0]
|
||||
if (!sel) return null
|
||||
|
||||
const [r1, , r2] = sel as [number, number, number, number]
|
||||
const startRow = Math.min(r1, r2)
|
||||
const endRow = Math.max(r1, r2)
|
||||
return { startRow, amount: endRow - startRow + 1 }
|
||||
}
|
||||
|
||||
const collectDescendantIds = (node: TreeNode): string[] => {
|
||||
const ids: string[] = []
|
||||
const stack: TreeNode[] = [...(node.__children ?? [])]
|
||||
while (stack.length) {
|
||||
const current = stack.pop()!
|
||||
if (current.__id) ids.push(current.__id)
|
||||
if (Array.isArray(current.__children) && current.__children.length) stack.push(...current.__children)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
const handleRowOperation = (hot: any, type: 'above' | 'below' | 'child'| 'append'|'delete') => {
|
||||
if (!hot) return
|
||||
const index = flatIndex.value
|
||||
if (!index) return
|
||||
|
||||
if (type === 'append') {
|
||||
index.root.push(createNode(hot.getSettings().dataSchema))
|
||||
rebuildDataFromTree(hot)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'delete') {
|
||||
const range = getSelectedVisualRowRange(hot)
|
||||
if (!range) return
|
||||
|
||||
const selected = index.metaByRow.slice(range.startRow, range.startRow + range.amount)
|
||||
const uniqueById = new Map<string, FlatRowMeta>()
|
||||
for (const meta of selected) uniqueById.set(meta.id, meta)
|
||||
|
||||
const metas = [...uniqueById.values()]
|
||||
metas.sort((a, b) => b.depth - a.depth)
|
||||
|
||||
const collapsed = collapsedNodeIds.value
|
||||
const bySiblings = new Map<TreeNode[], FlatRowMeta[]>()
|
||||
for (const meta of metas) {
|
||||
const list = bySiblings.get(meta.siblings)
|
||||
if (list) list.push(meta)
|
||||
else bySiblings.set(meta.siblings, [meta])
|
||||
}
|
||||
|
||||
for (const meta of metas) {
|
||||
for (const descendantId of collectDescendantIds(meta.node)) collapsed.delete(descendantId)
|
||||
collapsed.delete(meta.id)
|
||||
}
|
||||
|
||||
for (const [siblings, list] of bySiblings.entries()) {
|
||||
const sorted = [...list].sort((a, b) => b.indexWithinParent - a.indexWithinParent)
|
||||
for (const meta of sorted) siblings.splice(meta.indexWithinParent, 1)
|
||||
}
|
||||
|
||||
rebuildDataFromTree(hot)
|
||||
return
|
||||
}
|
||||
|
||||
const selection = hot.getSelectedLast?.() ?? hot.getSelected?.()?.[0]
|
||||
if (!selection) return
|
||||
const [r1] = selection as [number, number, number, number]
|
||||
const meta = index.metaByRow[r1]
|
||||
if (!meta) return
|
||||
|
||||
if (type === 'child') {
|
||||
meta.node.__children ??= []
|
||||
meta.node.__children.push(createNode(hot.getSettings().dataSchema))
|
||||
collapsedNodeIds.value.delete(meta.id)
|
||||
rebuildDataFromTree(hot)
|
||||
return
|
||||
}
|
||||
|
||||
meta.siblings.splice(type === 'above' ? meta.indexWithinParent : meta.indexWithinParent + 1, 0, createNode(hot.getSettings().dataSchema))
|
||||
rebuildDataFromTree(hot)
|
||||
}
|
||||
|
||||
const codeRenderer = (
|
||||
hot: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: any,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
// 检查是否需要设置验证背景色
|
||||
const cellMeta = hot.getCellMeta(row, col)
|
||||
const isValid = cellMeta?.valid !== false
|
||||
|
||||
// 如果单元格被标记为无效,设置红色背景
|
||||
if (!isValid) {
|
||||
TD.style.backgroundColor = '#ffbeba' // 淡红色背景
|
||||
} else {
|
||||
//TD.style.backgroundColor = cellProperties.className == "row-highlight"?'#e9ecfc':'' // 清除背景色
|
||||
}
|
||||
|
||||
const meta = flatIndex.value?.metaByRow?.[row]
|
||||
const key = meta?.lineKey ?? '0'
|
||||
|
||||
const { indentLayer, content, toggleEl, textEl } = getTreeCellDom(TD)
|
||||
if (indentLayer.dataset.paintKey !== key) {
|
||||
indentLayer.dataset.paintKey = key
|
||||
const paint = getTreeLinePaint(key)
|
||||
indentLayer.style.width = paint.width
|
||||
indentLayer.style.backgroundImage = paint.backgroundImage
|
||||
indentLayer.style.backgroundSize = paint.backgroundSize
|
||||
indentLayer.style.backgroundPosition = paint.backgroundPosition
|
||||
indentLayer.style.backgroundRepeat = paint.backgroundImage ? 'no-repeat' : ''
|
||||
}
|
||||
|
||||
if (meta?.hasChildren && meta?.id) {
|
||||
const isCollapsed = collapsedNodeIds.value.has(meta.id)
|
||||
const needsButton = toggleEl.tagName !== 'DIV' || !toggleEl.classList.contains('ht_nestingButton')
|
||||
const btn = needsButton ? document.createElement('div') : toggleEl
|
||||
btn.className = `ht_nestingButton ${isCollapsed ? 'ht_nestingExpand' : 'ht_nestingCollapse'}`
|
||||
btn.dataset.nodeId = meta.id
|
||||
// 确保按钮可以响应点击事件
|
||||
btn.style.pointerEvents = 'auto'
|
||||
if (!(btn as any).__htTreeToggleBound) {
|
||||
;(btn as any).__htTreeToggleBound = true
|
||||
btn.addEventListener('mousedown', (ev) => {
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
const nodeId = (ev.currentTarget as HTMLElement | null)?.dataset.nodeId
|
||||
if (!nodeId) return
|
||||
toggleNodeCollapsed(nodeId)
|
||||
})
|
||||
}
|
||||
if (needsButton) content.replaceChild(btn, toggleEl)
|
||||
} else {
|
||||
const needsSpacer = toggleEl.tagName !== 'SPAN' || !toggleEl.classList.contains('ht_treeToggleSpacer')
|
||||
if (needsSpacer) {
|
||||
const spacer = document.createElement('span')
|
||||
spacer.className = 'ht_treeToggleSpacer'
|
||||
content.replaceChild(spacer, toggleEl)
|
||||
}
|
||||
}
|
||||
|
||||
textEl.textContent = value == null ? '' : String(value)
|
||||
|
||||
return TD
|
||||
}
|
||||
|
||||
const levelRenderer = (
|
||||
hot: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: any,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
TD.textContent = flatIndex.value?.metaByRow?.[row]?.path ?? ''
|
||||
return TD
|
||||
}
|
||||
const initSchema = (columns: any[]) => {
|
||||
let rowSchema: any = {__id:null, level: null, __children: []}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.forEach((col: any) => {
|
||||
if (col.data && col.data !== 'level' && col.data !== '__children'&& col.data !== '__id') {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
return rowSchema
|
||||
}
|
||||
|
||||
//把最后一次选中的行重新选回来,保持高亮
|
||||
const highlightDeselect = (_this: any, coords: any) => {
|
||||
if (coords !== null) queueMicrotask(() => _this.selectCell(coords?.row, coords?.col))
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
initSchema,
|
||||
highlightDeselect,
|
||||
codeRenderer,
|
||||
levelRenderer,
|
||||
handleRowOperation,
|
||||
flatIndex,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///当设置 manualColumnResize: true 时,双击列边界触发自动适应列宽的方法是 autoColumnSize 插件。
|
||||
function applyDblClickAutoFit (hot: any, visualCol: number) {
|
||||
const autoFit = useDebounceFn(()=>{
|
||||
if (!hot || !Number.isInteger(visualCol)) return
|
||||
const manualColumnResize = hot.getPlugin?.('manualColumnResize')
|
||||
if (!manualColumnResize?.setManualSize) return
|
||||
|
||||
const baseWidth = hot.getColWidth?.(visualCol) ?? 0
|
||||
const hookedWidth = hot.runHooks?.('beforeColumnResize', baseWidth, visualCol, true)
|
||||
const newWidth = typeof hookedWidth === 'number' ? hookedWidth : baseWidth
|
||||
|
||||
manualColumnResize.setManualSize(visualCol, newWidth)
|
||||
manualColumnResize.saveManualColumnWidths?.()
|
||||
hot.runHooks?.('afterColumnResize', newWidth, visualCol, true)
|
||||
hot.view?.adjustElementsSize?.()
|
||||
hot.render?.()
|
||||
},300)
|
||||
autoFit()
|
||||
}
|
||||
export function applyAutoFitColum (hot: any, changes: any[]) {
|
||||
const columns = hot.getSettings?.().columns
|
||||
for (const change of changes) {
|
||||
const [, prop, oldValue, newValue] = change
|
||||
if (!columns || !Array.isArray(columns)) continue
|
||||
const visualCol = columns.findIndex((col) => col?.autoWidth === true && col?.data === prop)
|
||||
if (visualCol < 0) continue
|
||||
if (oldValue === newValue) continue
|
||||
applyDblClickAutoFit(hot, visualCol)
|
||||
console.log('scheduleDblClickAutoFit')
|
||||
break
|
||||
}
|
||||
}
|
||||
169
apps/web-ele/src/components/db-hst/popover.ts
Normal file
169
apps/web-ele/src/components/db-hst/popover.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
type SelectRenderDeps = {
|
||||
visible: Ref<boolean>
|
||||
buttonRef: Ref<HTMLElement | null | undefined>
|
||||
}
|
||||
|
||||
// 设置全局点击监听器的 Hook
|
||||
export const usePopoverClickOutside = (
|
||||
visible: Ref<boolean>,
|
||||
onClickOutside: () => void
|
||||
) => {
|
||||
const handler = (event: PointerEvent) => {
|
||||
visible.value = false
|
||||
onClickOutside()
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', handler, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('pointerdown', handler, true)
|
||||
})
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
const debounce = <T extends (...args: any[]) => void>(func: T, wait: number) => {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
const context = this
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
func.apply(context, args)
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
|
||||
export const createPopoverCellRenderer = ({ visible, buttonRef }: SelectRenderDeps) => {
|
||||
const openPopover = (container: HTMLElement, virtualEl: HTMLElement) => {
|
||||
buttonRef.value = virtualEl
|
||||
visible.value = true
|
||||
}
|
||||
return (
|
||||
instance: Handsontable,
|
||||
td: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: any,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
//parent存在并ture时,或者,没有parent对象时
|
||||
if((rowData.hasOwnProperty('parent') && rowData?.parent) || !rowData.hasOwnProperty('parent')){
|
||||
|
||||
td.innerHTML = ''
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.justifyContent = 'space-between'
|
||||
container.style.width = '100%'
|
||||
container.style.height = '100%'
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.type = 'text'
|
||||
input.value = value || ''
|
||||
input.style.flex = '1'
|
||||
input.style.border = 'none'
|
||||
input.style.outline = 'none'
|
||||
input.style.background = 'transparent'
|
||||
input.style.padding = '0'
|
||||
input.style.width = '100%'
|
||||
input.style.fontSize = 'inherit'
|
||||
input.style.fontFamily = 'inherit'
|
||||
|
||||
input.setAttribute('data-cell-row', row.toString())
|
||||
input.setAttribute('data-cell-col', col.toString())
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
input.addEventListener('keyup', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
input.addEventListener('keypress', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
input.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
input.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
openPopover(container, input)
|
||||
})
|
||||
|
||||
const debouncedInputHandler = debounce((newValue: string) => {
|
||||
console.log(`输入变化(防抖后) - 行: ${row}, 列: ${col}, 新值: ${newValue}`)
|
||||
|
||||
openPopover(container, input)
|
||||
|
||||
if (cellProperties.inputCallback && typeof cellProperties.inputCallback === 'function') {
|
||||
const rowData = (instance as any).getSourceDataAtRow(row)
|
||||
cellProperties.inputCallback(row, col, prop, newValue, rowData, instance, td)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const newValue = target.value
|
||||
|
||||
console.log(`输入中 - 行: ${row}, 列: ${col}, 新值: ${newValue}`)
|
||||
|
||||
debouncedInputHandler(newValue)
|
||||
})
|
||||
input.addEventListener('focus', () => {
|
||||
console.log(`输入框获得焦点 - 行: ${row}, 列: ${col}`)
|
||||
|
||||
openPopover(container, input)
|
||||
|
||||
if (cellProperties.focusCallback && typeof cellProperties.focusCallback === 'function') {
|
||||
const rowData = (instance as any).getSourceDataAtRow(row)
|
||||
cellProperties.focusCallback(row, col, prop, input.value, rowData, instance, td)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
console.log(`输入框失焦 - 行: ${row}, 列: ${col}, 最终值: ${input.value}`)
|
||||
})
|
||||
|
||||
const searchIcon = document.createElement('span')
|
||||
searchIcon.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>`
|
||||
searchIcon.style.cursor = 'pointer'
|
||||
searchIcon.style.marginLeft = '5px'
|
||||
searchIcon.style.fontSize = '14px'
|
||||
searchIcon.style.flexShrink = '0'
|
||||
|
||||
searchIcon.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
console.log(`搜索图标被点击 - 行: ${row}, 列: ${col}, 值: ${input.value}`)
|
||||
|
||||
openPopover(container, searchIcon)
|
||||
|
||||
if (cellProperties.selectCallback && typeof cellProperties.selectCallback === 'function') {
|
||||
const rowData = (instance as any).getSourceDataAtRow(row)
|
||||
cellProperties.selectCallback(row, col, prop, input.value, rowData, instance, td)
|
||||
}
|
||||
})
|
||||
|
||||
container.appendChild(input)
|
||||
container.appendChild(searchIcon)
|
||||
td.appendChild(container)
|
||||
}else{
|
||||
td.innerHTML = value || ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
return td
|
||||
}
|
||||
}
|
||||
61
apps/web-ele/src/components/db-hst/popover.vue
Normal file
61
apps/web-ele/src/components/db-hst/popover.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElPopover, ElTableV2 } from 'element-plus'
|
||||
import type { PopoverInstance, RowEventHandlers } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
buttonRef: HTMLElement | null | undefined
|
||||
columns: any[]
|
||||
data: any[]
|
||||
rowEventHandlers?: RowEventHandlers
|
||||
width?: number
|
||||
height?: number
|
||||
popperHeight?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 955,
|
||||
height: 350,
|
||||
popperHeight: '380px'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'end-reached': []
|
||||
}>()
|
||||
|
||||
const popoverRef = ref<PopoverInstance>()
|
||||
|
||||
// 暴露 popoverRef 供父组件调用
|
||||
defineExpose({ popoverRef })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElPopover
|
||||
ref="popoverRef"
|
||||
:virtual-ref="buttonRef"
|
||||
virtual-triggering
|
||||
:visible="visible"
|
||||
placement="bottom"
|
||||
:width="width"
|
||||
:popper-style="{ height: popperHeight }"
|
||||
>
|
||||
<ElTableV2
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:width="width - 25"
|
||||
:height="height"
|
||||
fixed
|
||||
:row-height="25"
|
||||
:header-height="25"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
:cell-props="{
|
||||
style: {
|
||||
background: 'transparent !important'
|
||||
}
|
||||
}"
|
||||
:teleported="false"
|
||||
@end-reached="emit('end-reached')"
|
||||
/>
|
||||
</ElPopover>
|
||||
</template>
|
||||
@@ -4,31 +4,62 @@ let currentAnchor: { instance: any; row: number; col: number; td: HTMLTableCellE
|
||||
|
||||
const clamp = (x: number, min: number, max: number) => (x < min ? min : x > max ? max : x)
|
||||
|
||||
/**
|
||||
* 获取嵌套属性值的辅助函数
|
||||
* @param obj 对象
|
||||
* @param path 属性路径,如 'calcBase.formula'
|
||||
* @returns 属性值
|
||||
*/
|
||||
const getNestedValue = (obj: any, path: string): any => {
|
||||
if (!path || !obj) return ''
|
||||
|
||||
const keys = path.split('.')
|
||||
let value = obj
|
||||
|
||||
for (const key of keys) {
|
||||
if (value === null || value === undefined) return ''
|
||||
value = value[key]
|
||||
}
|
||||
|
||||
return value ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建表格数据结构的辅助函数
|
||||
* @param dataSource 数据源数组
|
||||
* @param fieldKeys 字段键数组,按顺序对应表格列
|
||||
* @param getLabelFn 获取标签的函数
|
||||
* @param getDisplayText 获取显示文本的函数
|
||||
* @returns 格式化后的表格行HTML和数据属性
|
||||
*/
|
||||
export function createTableDataStructure(
|
||||
dataSource: any[],
|
||||
fieldKeys: string[],
|
||||
getLabelFn?: (item: any) => string
|
||||
getDisplayText?: (item: any) => string
|
||||
) {
|
||||
const getLabel = getLabelFn ?? ((x: any) => x?.name ?? '')
|
||||
const getLabel = getDisplayText ?? ((x: any) => x?.name ?? '')
|
||||
|
||||
return dataSource.map(item => {
|
||||
// 动态生成单元格
|
||||
const cells = fieldKeys.map(key => `<td>${String(item?.[key] ?? '')}</td>`).join('')
|
||||
// 动态生成单元格 - 支持嵌套属性
|
||||
const cells = fieldKeys.map(key => {
|
||||
const value = getNestedValue(item, key)
|
||||
return `<td>${String(value)}</td>`
|
||||
}).join('')
|
||||
|
||||
// 动态生成 data 属性
|
||||
// 动态生成 data 属性 - 支持嵌套属性
|
||||
const dataAttrs = fieldKeys
|
||||
.map(key => `data-${key.toLowerCase()}="${String(item?.[key] ?? '')}"`)
|
||||
.map(key => {
|
||||
const value = getNestedValue(item, key)
|
||||
// 将嵌套路径转换为有效的 data 属性名,如 'calcBase.formula' -> 'calcbase-formula'
|
||||
const attrName = key.toLowerCase().replace(/\./g, '-')
|
||||
return `data-${attrName}="${String(value)}"`
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
// 将完整的 item 数据序列化存储
|
||||
const itemDataJson = JSON.stringify(item).replace(/"/g, '"')
|
||||
|
||||
return {
|
||||
html: `<tr class="hot-dropdown-row" data-label="${String(getLabel(item) ?? '')}" ${dataAttrs}>${cells}</tr>`,
|
||||
html: `<tr class="hot-dropdown-row" data-label="${String(getLabel(item) ?? '')}" data-item="${itemDataJson}" ${dataAttrs}>${cells}</tr>`,
|
||||
data: item
|
||||
}
|
||||
})
|
||||
@@ -93,21 +124,18 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
||||
return el
|
||||
}
|
||||
|
||||
const labelFn: ((x: any) => string) | undefined = cellProperties.customGetLabel
|
||||
|
||||
const display = createEl('div', 'hot-dropdown-display')
|
||||
const labelText = typeof value === 'string' ? value : labelFn?.(value) ?? (value && typeof value === 'object' ? String(value.name ?? '') : '')
|
||||
const labelText = typeof value === 'string' ? value : (value && typeof value === 'object' ? String(value.name ?? '') : '')
|
||||
if (labelText && labelText.length > 0) display.appendChild(createEl('span', 'hot-dropdown-text', labelText))
|
||||
else display.appendChild(createEl('span', 'hot-dropdown-placeholder', '选择'))
|
||||
|
||||
const trigger = createEl('button', 'hot-dropdown-trigger', '▼') as HTMLButtonElement
|
||||
trigger.type = 'button'
|
||||
|
||||
const buildDropdown = () => {
|
||||
const src = cellProperties.customDropdownSource as any[] | undefined
|
||||
const buildDropdown = async () => {
|
||||
const headers: string[] | undefined = cellProperties.customTableHeaders
|
||||
const dropdown = createEl('div', 'hot-dropdown')
|
||||
const getLabel = labelFn ?? ((x: any) => x?.name ?? '')
|
||||
const getDisplayText = (x: any) => x?.name ?? ''
|
||||
|
||||
// 创建搜索栏
|
||||
const searchContainer = createEl('div', 'hot-dropdown-search')
|
||||
@@ -119,29 +147,51 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
||||
const theadHtml = headers && headers.length ? `<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>` : ''
|
||||
|
||||
// 使用自定义字段键或默认字段键
|
||||
const fieldKeys = cellProperties.customFieldKeys || ['code', 'name', 'spec', 'category', 'unit', 'taxRate', 'priceExTax', 'priceInTax', 'priceExTaxComp', 'priceInTaxComp', 'calcBase']
|
||||
|
||||
const rowsHtml = Array.isArray(src)
|
||||
? createTableDataStructure(src, fieldKeys, labelFn).map(row => row.html).join('')
|
||||
: ''
|
||||
const fieldKeys = cellProperties.customFieldKeys || []
|
||||
|
||||
// 创建加载提示
|
||||
const tableEl = createEl('div', 'hot-dropdown-table-wrapper')
|
||||
tableEl.innerHTML = `<table class="hot-dropdown-table">${theadHtml}<tbody>${rowsHtml}</tbody></table>`
|
||||
tableEl.innerHTML = `<table class="hot-dropdown-table">${theadHtml}<tbody><tr><td colspan="${fieldKeys.length}" style="text-align: center; padding: 20px;">加载中...</td></tr></tbody></table>`
|
||||
|
||||
dropdown.appendChild(searchContainer)
|
||||
dropdown.appendChild(tableEl)
|
||||
|
||||
const tbody = dropdown.querySelector('tbody') as HTMLTableSectionElement
|
||||
const allRows = Array.from(tbody.querySelectorAll('tr')) as HTMLTableRowElement[]
|
||||
// 异步加载数据
|
||||
let src: any[] = []
|
||||
const dataSource = cellProperties.customDropdownSource
|
||||
|
||||
// 搜索功能 - 动态搜索所有字段
|
||||
if (typeof dataSource === 'function') {
|
||||
try {
|
||||
src = await dataSource()
|
||||
} catch (error) {
|
||||
console.error('加载下拉数据失败:', error)
|
||||
tableEl.innerHTML = `<table class="hot-dropdown-table">${theadHtml}<tbody><tr><td colspan="${fieldKeys.length}" style="text-align: center; padding: 20px; color: red;">加载失败</td></tr></tbody></table>`
|
||||
return dropdown
|
||||
}
|
||||
} else if (Array.isArray(dataSource)) {
|
||||
src = dataSource
|
||||
}
|
||||
|
||||
// 渲染数据
|
||||
const rowsHtml = Array.isArray(src) && src.length > 0
|
||||
? createTableDataStructure(src, fieldKeys, getDisplayText).map(row => row.html).join('')
|
||||
: `<tr><td colspan="${fieldKeys.length}" style="text-align: center; padding: 20px;">暂无数据</td></tr>`
|
||||
|
||||
tableEl.innerHTML = `<table class="hot-dropdown-table">${theadHtml}<tbody>${rowsHtml}</tbody></table>`
|
||||
|
||||
const tbody = dropdown.querySelector('tbody') as HTMLTableSectionElement
|
||||
const allRows = Array.from(tbody.querySelectorAll('tr.hot-dropdown-row')) as HTMLTableRowElement[]
|
||||
|
||||
// 搜索功能 - 动态搜索所有字段(支持嵌套属性)
|
||||
const searchFieldKeys = cellProperties.customSearchFields || cellProperties.customFieldKeys || ['code', 'name', 'spec', 'category', 'unit']
|
||||
searchInput.addEventListener('input', (ev) => {
|
||||
ev.stopPropagation()
|
||||
const keyword = searchInput.value.toLowerCase().trim()
|
||||
allRows.forEach(tr => {
|
||||
const matches = searchFieldKeys.some((key: string) => {
|
||||
const value = (tr.dataset[key.toLowerCase()] ?? '').toLowerCase()
|
||||
// 将嵌套路径转换为 data 属性名,如 'calcBase.formula' -> 'calcbase-formula'
|
||||
const attrName = key.toLowerCase().replace(/\./g, '-')
|
||||
const value = (tr.dataset[attrName] ?? '').toLowerCase()
|
||||
return value.includes(keyword)
|
||||
})
|
||||
tr.style.display = matches ? '' : 'none'
|
||||
@@ -154,9 +204,25 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
||||
tbody.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation()
|
||||
const tr = (ev.target as HTMLElement).closest('tr') as HTMLTableRowElement | null
|
||||
if (!tr) return
|
||||
const next = tr.dataset.label ?? ''
|
||||
instance.setDataAtCell(row, column, next)
|
||||
if (!tr || !tr.classList.contains('hot-dropdown-row')) return
|
||||
|
||||
// 从 data-item 属性中恢复完整的 item 数据
|
||||
const itemJson = tr.dataset.item ?? '{}'
|
||||
const selectedItem = JSON.parse(itemJson.replace(/"/g, '"'))
|
||||
|
||||
// 获取当前行的完整数据
|
||||
const currentRowData = instance.getSourceDataAtRow(row)
|
||||
|
||||
// 调用自定义回调函数
|
||||
const callbackFn = cellProperties.customCallbackRow
|
||||
if (typeof callbackFn === 'function') {
|
||||
callbackFn(currentRowData, selectedItem, row, column, instance)
|
||||
} else {
|
||||
// 默认行为:设置显示文本
|
||||
const displayValue = tr.dataset.label ?? ''
|
||||
instance.setDataAtCell(row, column, displayValue)
|
||||
}
|
||||
|
||||
if (dropdown.parentNode) dropdown.parentNode.removeChild(dropdown)
|
||||
currentDropdownEl = null
|
||||
if (currentOnDocClick) {
|
||||
@@ -171,11 +237,11 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
||||
return dropdown
|
||||
}
|
||||
|
||||
const openDropdown = () => {
|
||||
const openDropdown = async () => {
|
||||
if (currentDropdownEl && currentDropdownEl.parentNode) currentDropdownEl.parentNode.removeChild(currentDropdownEl)
|
||||
if (currentOnDocClick) document.removeEventListener('click', currentOnDocClick)
|
||||
|
||||
const dropdown = buildDropdown()
|
||||
const dropdown = await buildDropdown()
|
||||
document.body.appendChild(dropdown)
|
||||
currentDropdownEl = dropdown
|
||||
currentAnchor = { instance, row, col: column, td }
|
||||
|
||||
28
apps/web-ele/src/components/db-hst/text.ts
Normal file
28
apps/web-ele/src/components/db-hst/text.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
export function handlerDuplicateCodeRenderer(instance: any, td: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: any){
|
||||
td.textContent = value || ''
|
||||
// 检查是否需要设置验证背景色
|
||||
const cellMeta = instance.getCellMeta(row, column)
|
||||
const isValid = cellMeta?.valid !== false
|
||||
|
||||
// 如果单元格被标记为无效,设置红色背景
|
||||
if (!isValid) {
|
||||
td.style.backgroundColor = '#fa4d3233' // 淡红色背景
|
||||
} else {
|
||||
td.style.backgroundColor = '' // 清除背景色
|
||||
}
|
||||
// 检查当前值是否重复(排除空值)
|
||||
if (value && value.toString().trim() !== '') {
|
||||
// 获取所有数据
|
||||
const data = instance.getSourceData()
|
||||
const duplicateCount = data.filter((item: any) => item.code === value).length
|
||||
if (duplicateCount > 1) {
|
||||
td.style.color = 'red'
|
||||
td.style.fontWeight = 'bold'
|
||||
} else {
|
||||
td.style.color = ''
|
||||
td.style.fontWeight = ''
|
||||
}
|
||||
}
|
||||
return td
|
||||
}
|
||||
@@ -1,3 +1,38 @@
|
||||
import { getTreeLine, getTreeLinePaint, getTreeCellDom } from './treeLine'
|
||||
|
||||
// 通用的验证 renderer,用于显示验证背景色
|
||||
export const validationRenderer = (
|
||||
hot: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: any,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
// 清空单元格
|
||||
while (TD.firstChild) TD.removeChild(TD.firstChild)
|
||||
|
||||
// 检查是否需要设置验证背景色
|
||||
const columns = this.getSettings().columns || []
|
||||
const currentColumn = columns[col]
|
||||
const isEmpty = value === null || value === undefined || String(value).trim() === ''
|
||||
|
||||
// 如果列配置了 allowInvalid: true 且值为空,设置红色背景
|
||||
if (currentColumn?.allowInvalid === true && isEmpty) {
|
||||
TD.style.backgroundColor = '#ff4d4f20' // 淡红色背景
|
||||
} else {
|
||||
TD.style.backgroundColor = '' // 清除背景色
|
||||
}
|
||||
|
||||
// 创建文本节点
|
||||
const text = document.createElement('span')
|
||||
text.textContent = value == null ? '' : String(value)
|
||||
TD.appendChild(text)
|
||||
|
||||
return TD
|
||||
}
|
||||
|
||||
export const codeRenderer = (
|
||||
hot: any,
|
||||
TD: HTMLTableCellElement,
|
||||
@@ -7,43 +42,71 @@ export const codeRenderer = (
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
while (TD.firstChild) TD.removeChild(TD.firstChild)
|
||||
|
||||
// 检查是否需要设置验证背景色
|
||||
const cellMeta = hot.getCellMeta(row, col)
|
||||
const isValid = cellMeta?.valid !== false
|
||||
|
||||
// 如果单元格被标记为无效,设置红色背景
|
||||
if (!isValid) {
|
||||
TD.style.backgroundColor = '#fa4d3233' // 淡红色背景
|
||||
} else {
|
||||
TD.style.backgroundColor = cellProperties.className == "row-highlight"?'#e9ecfc':'' // 清除背景色
|
||||
}
|
||||
|
||||
const nestedRowsPlugin = hot.getPlugin('nestedRows')
|
||||
const physicalRow = nestedRowsPlugin ? nestedRowsPlugin.collapsingUI.translateTrimmedRow(row) : hot.toPhysicalRow(row)
|
||||
const rowObj = hot.getSourceDataAtRow(physicalRow)
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.gap = '6px'
|
||||
const level = nestedRowsPlugin?.dataManager.getRowLevel(physicalRow) ?? 0
|
||||
for (let i = 0; i < (level || 0); i++) {
|
||||
const spacer = document.createElement('span')
|
||||
spacer.className = 'ht_nestingLevel_empty'
|
||||
container.appendChild(spacer)
|
||||
const dataManager = nestedRowsPlugin?.dataManager
|
||||
|
||||
const dataNode = dataManager?.getDataObject?.(physicalRow)
|
||||
const root = (dataManager?.getData?.() as any[] | undefined) ?? []
|
||||
const line = nestedRowsPlugin && dataManager && dataNode && Array.isArray(root)
|
||||
? getTreeLine(dataNode, dataManager, root)
|
||||
: ({ key: '0', hasChildren: false } as const)
|
||||
|
||||
const { indentLayer, content, toggleEl, textEl } = getTreeCellDom(TD)
|
||||
if (indentLayer.dataset.paintKey !== line.key) {
|
||||
indentLayer.dataset.paintKey = line.key
|
||||
const paint = getTreeLinePaint(line.key)
|
||||
indentLayer.style.width = paint.width
|
||||
indentLayer.style.backgroundImage = paint.backgroundImage
|
||||
indentLayer.style.backgroundSize = paint.backgroundSize
|
||||
indentLayer.style.backgroundPosition = paint.backgroundPosition
|
||||
indentLayer.style.backgroundRepeat = paint.backgroundImage ? 'no-repeat' : ''
|
||||
}
|
||||
if (rowObj && Array.isArray(rowObj.__children) && rowObj.__children.length > 0) {
|
||||
|
||||
const isCollapsed = nestedRowsPlugin?.collapsingUI.areChildrenCollapsed(physicalRow) ?? false
|
||||
const btn = document.createElement('div')
|
||||
|
||||
if (line.hasChildren && nestedRowsPlugin) {
|
||||
const isCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(physicalRow)
|
||||
const needsButton = toggleEl.tagName !== 'DIV' || !toggleEl.classList.contains('ht_nestingButton')
|
||||
const btn = needsButton ? document.createElement('div') : toggleEl
|
||||
btn.className = `ht_nestingButton ${isCollapsed ? 'ht_nestingExpand' : 'ht_nestingCollapse'}`
|
||||
btn.addEventListener('mousedown', (ev) => {
|
||||
if (ev.button !== 0) return
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
if (!nestedRowsPlugin) return
|
||||
const nowCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(physicalRow)
|
||||
if (nowCollapsed) nestedRowsPlugin.collapsingUI.expandChildren(physicalRow)
|
||||
else nestedRowsPlugin.collapsingUI.collapseChildren(physicalRow)
|
||||
})
|
||||
container.appendChild(btn)
|
||||
}/*else{
|
||||
container.classList.add('text-relative')
|
||||
}*/
|
||||
const text = document.createElement('span')
|
||||
text.textContent = value == null ? '' : String(value)
|
||||
text.classList.add('rowHeader')
|
||||
btn.dataset.row = String(physicalRow)
|
||||
if (!(btn as any).__htTreeToggleBound) {
|
||||
;(btn as any).__htTreeToggleBound = true
|
||||
btn.addEventListener('mousedown', (ev) => {
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
const rowStr = (ev.currentTarget as HTMLElement | null)?.dataset.row
|
||||
const targetRow = rowStr ? Number(rowStr) : NaN
|
||||
if (!nestedRowsPlugin || Number.isNaN(targetRow)) return
|
||||
const nowCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(targetRow)
|
||||
if (nowCollapsed) nestedRowsPlugin.collapsingUI.expandChildren(targetRow)
|
||||
else nestedRowsPlugin.collapsingUI.collapseChildren(targetRow)
|
||||
})
|
||||
}
|
||||
if (needsButton) content.replaceChild(btn, toggleEl)
|
||||
} else {
|
||||
const needsSpacer = toggleEl.tagName !== 'SPAN' || !toggleEl.classList.contains('ht_treeToggleSpacer')
|
||||
if (needsSpacer) {
|
||||
const spacer = document.createElement('span')
|
||||
spacer.className = 'ht_treeToggleSpacer'
|
||||
content.replaceChild(spacer, toggleEl)
|
||||
}
|
||||
}
|
||||
|
||||
textEl.textContent = value == null ? '' : String(value)
|
||||
//解决右键行头触发上下文菜单事件
|
||||
text.addEventListener('contextmenu', (ev) => {
|
||||
textEl.addEventListener('contextmenu', (ev) => {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const e = new MouseEvent('contextmenu', {
|
||||
@@ -56,19 +119,52 @@ export const codeRenderer = (
|
||||
})
|
||||
TD.dispatchEvent(e)
|
||||
})
|
||||
container.appendChild(text)
|
||||
TD.appendChild(container)
|
||||
}
|
||||
|
||||
return TD
|
||||
// codeRenderer 的编辑后回调处理
|
||||
export const handleCodeCallback = (hot: any, changes: any[], codeCallbackRow?: Function) => {
|
||||
if (!changes || !codeCallbackRow) return
|
||||
|
||||
const columns = hot.getSettings().columns || []
|
||||
|
||||
changes.forEach(([row, prop, oldValue, newValue]) => {
|
||||
// 查找当前列配置
|
||||
const colIndex = hot.propToCol(prop)
|
||||
const currentColumn = columns[colIndex]
|
||||
|
||||
//console.log('currentColumn?.code',currentColumn?.code)
|
||||
// 只处理配置了 code: true 的列的变化
|
||||
if (currentColumn?.code === true && oldValue !== newValue) {
|
||||
const nestedRowsPlugin = hot.getPlugin('nestedRows')
|
||||
const physicalRow = nestedRowsPlugin ? nestedRowsPlugin.collapsingUI.translateTrimmedRow(row) : hot.toPhysicalRow(row)
|
||||
const currentRowData = hot.getSourceDataAtRow(physicalRow)
|
||||
|
||||
// 调用回调函数,传递参数:当前行数据、旧值、新值、行索引、列索引、实例
|
||||
if (typeof codeCallbackRow === 'function') {
|
||||
codeCallbackRow(currentRowData, oldValue, newValue, row, colIndex, hot)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const computeCodeColWidth = (hot: any): number => {
|
||||
// if (!hot) return codeColWidth.value
|
||||
if (!hot) return 120
|
||||
|
||||
// 查找配置了 code: true 的列
|
||||
const columns = hot.getSettings().columns || []
|
||||
const codeColumn = columns.find((col: any) => col.code === true)
|
||||
|
||||
if (!codeColumn || !codeColumn.data) return 120
|
||||
|
||||
const data = hot.getSourceData() || []
|
||||
const codeDataKey = codeColumn.data
|
||||
|
||||
// 计算该列的最大长度
|
||||
const maxLen = data.reduce((m: number, r: any) => {
|
||||
const v = r && r.code != null ? String(r.code) : ''
|
||||
const v = r && r[codeDataKey] != null ? String(r[codeDataKey]) : ''
|
||||
return Math.max(m, v.length)
|
||||
}, 0)
|
||||
|
||||
const charWidth = 9
|
||||
const basePadding = 24
|
||||
const nested = hot.getPlugin('nestedRows')
|
||||
@@ -84,100 +180,71 @@ export const computeCodeColWidth = (hot: any): number => {
|
||||
return Math.min(Math.max(80, width), 480)
|
||||
}
|
||||
|
||||
// 工具函数:解析 level 字符串
|
||||
const parseLevel = (level: string): number[] =>
|
||||
level?.split('-').map(n => Number(n)).filter(n => !Number.isNaN(n)) ?? []
|
||||
|
||||
// 工具函数:根据 level 获取容器和索引
|
||||
const getContainerAndIndexByLevel = (data: any[], level: string) => {
|
||||
const seg = parseLevel(level)
|
||||
if (seg.length === 0) return null
|
||||
if (seg.length === 1) return { container: data, index: seg[0], parentLevel: null }
|
||||
|
||||
let parent = data[seg[0]]
|
||||
for (let i = 1; i < seg.length - 1; i++) {
|
||||
if (!Array.isArray(parent.__children)) parent.__children = []
|
||||
parent = parent.__children[seg[i] - 1]
|
||||
if (!parent) return null
|
||||
const createNode = (dataSchema: any,level: string): any => ({
|
||||
...dataSchema,
|
||||
level,
|
||||
__children: []
|
||||
})
|
||||
const getSelectedVisualRowRange = (hot: any): { startRow: number; amount: number } | null => {
|
||||
const sel = hot.getSelectedLast?.() ?? hot.getSelected?.()?.[0]
|
||||
if (!sel) return null
|
||||
|
||||
const [r1, , r2] = sel as [number, number, number, number]
|
||||
const startRow = Math.min(r1, r2)
|
||||
const endRow = Math.max(r1, r2)
|
||||
return { startRow, amount: endRow - startRow + 1 }
|
||||
}
|
||||
/** 生成树结构 */
|
||||
const computeRowPath = (node: any, dm: any, root: any[]): string => {
|
||||
const chain: any[] = []
|
||||
for (let n = node; n; n = dm.getRowParent(n)) chain.unshift(n)
|
||||
const segs: string[] = [String(root.indexOf(chain[0]))]
|
||||
for (let i = 1; i < chain.length; i++) segs.push(String((chain[i - 1].__children?.indexOf(chain[i]) ?? -1) + 1))
|
||||
return segs.join('-')
|
||||
}
|
||||
|
||||
export const handleRowOperation = (hot: any, type: 'above' | 'below' | 'child' | 'append' | 'delete') => {
|
||||
//空白处右键 新增行
|
||||
if (type === 'append') {
|
||||
//const root = hot.getSourceData() as any[]
|
||||
// 使用正确的 level 格式:索引值(从0开始)
|
||||
// hot.getSettings().data.push(createNode(hot.getSettings().dataSchema, "0"))
|
||||
// hot.loadData(hot.getSettings().data)
|
||||
// console.log('append',root,hot.getSettings().data)
|
||||
const root = hot.getSourceData() as any[]
|
||||
hot.alter('insert_row_below', root.length, 1, 'insert_row_alter')
|
||||
console.log('append',root)
|
||||
return
|
||||
}
|
||||
|
||||
const container = Array.isArray(parent.__children) ? parent.__children : (parent.__children = [])
|
||||
const index = seg[seg.length - 1] - 1
|
||||
const parentLevel = seg.slice(0, -1).join('-')
|
||||
return { container, index, parentLevel }
|
||||
}
|
||||
|
||||
// 工具函数:根据 level 查找节点
|
||||
const findNodeByLevel = (data: any[], level: string) => {
|
||||
const loc = getContainerAndIndexByLevel(data, level)
|
||||
return loc ? (loc.container[loc.index] ?? null) : null
|
||||
}
|
||||
|
||||
// 工具函数:重新索引 level
|
||||
const reindexLevels = (container: any[], parentLevel: string | null): void => {
|
||||
container.forEach((row, i) => {
|
||||
const currentLevel = parentLevel == null ? String(i) : `${parentLevel}-${i + 1}`
|
||||
row.level = currentLevel
|
||||
if (Array.isArray(row.__children) && row.__children.length > 0) {
|
||||
reindexLevels(row.__children, currentLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 统一的行操作处理函数
|
||||
export const handleRowOperation = (hot: any, type: 'above' | 'below' | 'child' | 'delete') => {
|
||||
const selected = hot.getSelected()
|
||||
if (!selected || selected.length === 0) return
|
||||
|
||||
const row = selected[0][0]
|
||||
const nestedRowsPlugin = hot.getPlugin('nestedRows')
|
||||
if (!nestedRowsPlugin) return
|
||||
|
||||
const physicalRow = nestedRowsPlugin ? nestedRowsPlugin.collapsingUI.translateTrimmedRow(row) : hot.toPhysicalRow(row)
|
||||
const rowData = hot.getSourceDataAtRow(physicalRow)
|
||||
const currentLevel = String(rowData.level ?? '')
|
||||
const data = JSON.parse(JSON.stringify(hot.getSettings().data))
|
||||
if (type === 'delete') {
|
||||
// 删除行
|
||||
const loc = getContainerAndIndexByLevel(data, currentLevel)
|
||||
if (!loc) return
|
||||
const { container, index, parentLevel } = loc
|
||||
container.splice(index, 1)
|
||||
reindexLevels(container, parentLevel)
|
||||
} else {
|
||||
// 根据 columns 配置动态生成 newRow 对象结构
|
||||
const columns = hot.getSettings().columns || []
|
||||
const newRow: any = {
|
||||
level: null,
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.forEach((col: any) => {
|
||||
if (col.data && col.data !== 'level' && col.data !== '__children') {
|
||||
newRow[col.data] = null
|
||||
}
|
||||
})
|
||||
|
||||
if (type === 'child') {
|
||||
// 添加子行
|
||||
const parentNode = findNodeByLevel(data, currentLevel)
|
||||
if (!parentNode) return
|
||||
if (!Array.isArray(parentNode.__children)) parentNode.__children = []
|
||||
const nextIndex = parentNode.__children.length + 1
|
||||
newRow.level = `${currentLevel}-${nextIndex}`
|
||||
parentNode.__children.push(newRow)
|
||||
} else {
|
||||
// 在上方或下方插入行
|
||||
const loc = getContainerAndIndexByLevel(data, currentLevel)
|
||||
if (!loc) return
|
||||
const { container, index, parentLevel } = loc
|
||||
const insertIndex = type === 'above' ? Math.max(index, 0) : Math.max(index + 1, 0)
|
||||
container.splice(insertIndex, 0, newRow)
|
||||
reindexLevels(container, parentLevel)
|
||||
}
|
||||
const range = getSelectedVisualRowRange(hot)
|
||||
if (!range) return
|
||||
hot.alter('remove_row', range.startRow, range.amount, 'remove_row_alter')
|
||||
return
|
||||
}
|
||||
hot.updateSettings({ data })
|
||||
// nestedRowsPlugin.headersUI.updateRowHeaderWidth()
|
||||
hot.render()
|
||||
const sel = hot.getSelected()
|
||||
if (!sel?.length) return
|
||||
const plugin = hot.getPlugin('nestedRows')
|
||||
if (!plugin) return
|
||||
const dm = plugin.dataManager
|
||||
const row = plugin.collapsingUI.translateTrimmedRow(sel[0][0])
|
||||
const target = dm.getDataObject(row)
|
||||
const parent = dm.getRowParent(row)
|
||||
const root = dm.getRawSourceData() as any[]
|
||||
|
||||
console.log('target',target)
|
||||
if (type === 'child') {
|
||||
const base = target.level && typeof target.level === 'string' ? target.level : computeRowPath(target, dm, root)
|
||||
const next = (target.__children?.length ?? 0) + 1
|
||||
;(target.__children ??= []).push(createNode(hot.getSettings().dataSchema,`${base}-${next}`))
|
||||
hot.loadData(root)
|
||||
return
|
||||
}
|
||||
|
||||
const list = parent ? parent.__children : root
|
||||
const pos = dm.getRowIndexWithinParent(row)
|
||||
const lvl = String(dm.getRowLevel(row) ?? 0)
|
||||
list.splice(type === 'above' ? pos : pos + 1, 0, createNode(hot.getSettings().dataSchema,lvl))
|
||||
hot.loadData(root)
|
||||
}
|
||||
|
||||
140
apps/web-ele/src/components/db-hst/treeLine.ts
Normal file
140
apps/web-ele/src/components/db-hst/treeLine.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
type TreeLinePaint = {
|
||||
key: string
|
||||
width: string
|
||||
backgroundImage: string
|
||||
backgroundSize: string
|
||||
backgroundPosition: string
|
||||
}
|
||||
|
||||
let treeLineCache = new WeakMap<any, { key: string; hasChildren: boolean }>()
|
||||
const treeLinePaintCache = new Map<string, TreeLinePaint>()
|
||||
|
||||
export const resetTreeLineCaches = () => {
|
||||
treeLineCache = new WeakMap<any, { key: string; hasChildren: boolean }>()
|
||||
treeLinePaintCache.clear()
|
||||
}
|
||||
|
||||
export const getTreeLine = (node: any, dataManager: any, root: any[]) => {
|
||||
const cached = treeLineCache.get(node)
|
||||
if (cached) return cached
|
||||
|
||||
const level = Math.max(0, dataManager.getRowLevel(node) ?? 0)
|
||||
const hasChildren = !!(node && Array.isArray(node.__children) && node.__children.length)
|
||||
|
||||
if (level <= 0) {
|
||||
const next = { key: '0', hasChildren }
|
||||
treeLineCache.set(node, next)
|
||||
return next
|
||||
}
|
||||
|
||||
const isLast = (n: any) => {
|
||||
const parent = dataManager.getRowParent(n)
|
||||
const siblings = (parent ? parent.__children : root) as any[] | undefined
|
||||
if (!Array.isArray(siblings) || siblings.length === 0) return true
|
||||
return siblings[siblings.length - 1] === n
|
||||
}
|
||||
|
||||
const flags: boolean[] = []
|
||||
let parent = dataManager.getRowParent(node)
|
||||
while (parent && (dataManager.getRowLevel(parent) ?? 0) > 0) {
|
||||
flags.push(!isLast(parent))
|
||||
parent = dataManager.getRowParent(parent)
|
||||
}
|
||||
flags.reverse()
|
||||
|
||||
const key = `${level}|${isLast(node) ? 1 : 0}|${flags.map(v => (v ? '1' : '0')).join('')}`
|
||||
const next = { key, hasChildren }
|
||||
treeLineCache.set(node, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export const getTreeLinePaint = (key: string) => {
|
||||
const cached = treeLinePaintCache.get(key)
|
||||
if (cached) return cached
|
||||
|
||||
if (key === '0') {
|
||||
const empty = { key, width: '0px', backgroundImage: '', backgroundSize: '', backgroundPosition: '' }
|
||||
treeLinePaintCache.set(key, empty)
|
||||
return empty
|
||||
}
|
||||
|
||||
const [levelStr, isLastStr, flagsStr] = key.split('|')
|
||||
const level = Number(levelStr)
|
||||
const isLast = isLastStr === '1'
|
||||
const flags = flagsStr ? flagsStr.split('').map(v => v === '1') : []
|
||||
|
||||
const color = 'var(--ht-tree-line-color)'
|
||||
const width = 'var(--ht-tree-line-width)'
|
||||
const indent = 'var(--ht-tree-indent)'
|
||||
|
||||
const images: string[] = []
|
||||
const sizes: string[] = []
|
||||
const positions: string[] = []
|
||||
|
||||
for (let i = 0; i < flags.length; i++) {
|
||||
if (!flags[i]) continue
|
||||
images.push(`linear-gradient(${color}, ${color})`)
|
||||
sizes.push(`${width} 100%`)
|
||||
positions.push(`calc(${indent} * ${i} + (${indent} / 2)) 0`)
|
||||
}
|
||||
|
||||
const selfDepth = level - 1
|
||||
images.push(`linear-gradient(${color}, ${color})`)
|
||||
sizes.push(`${width} ${isLast ? '50%' : '100%'}`)
|
||||
positions.push(`calc(${indent} * ${selfDepth} + (${indent} / 2)) 0`)
|
||||
|
||||
images.push(`linear-gradient(to right, ${color}, ${color})`)
|
||||
sizes.push(`calc(${indent} / 2) ${width}`)
|
||||
positions.push(`calc(${indent} * ${selfDepth} + (${indent} / 2)) 50%`)
|
||||
|
||||
const paint = {
|
||||
key,
|
||||
width: `calc(var(--ht-tree-indent) * ${level})`,
|
||||
backgroundImage: images.join(', '),
|
||||
backgroundSize: sizes.join(', '),
|
||||
backgroundPosition: positions.join(', '),
|
||||
}
|
||||
treeLinePaintCache.set(key, paint)
|
||||
return paint
|
||||
}
|
||||
|
||||
export const getTreeCellDom = (TD: HTMLTableCellElement) => {
|
||||
const currentTreeCell = TD.firstElementChild as HTMLElement | null
|
||||
if (currentTreeCell && currentTreeCell.classList.contains('ht_treeCell')) {
|
||||
const indentLayer = currentTreeCell.firstElementChild as HTMLElement
|
||||
const content = currentTreeCell.lastElementChild as HTMLElement
|
||||
return {
|
||||
treeCell: currentTreeCell,
|
||||
indentLayer,
|
||||
content,
|
||||
toggleEl: content.firstElementChild as HTMLElement,
|
||||
textEl: content.lastElementChild as HTMLElement,
|
||||
}
|
||||
}
|
||||
|
||||
const treeCell = document.createElement('div')
|
||||
treeCell.className = 'ht_treeCell'
|
||||
|
||||
const indentLayer = document.createElement('span')
|
||||
indentLayer.className = 'ht_treeIndentLayer'
|
||||
|
||||
const content = document.createElement('div')
|
||||
content.className = 'ht_treeContent'
|
||||
const toggleSpacer = document.createElement('span')
|
||||
toggleSpacer.className = 'ht_treeToggleSpacer'
|
||||
const text = document.createElement('span')
|
||||
text.className = 'rowHeader'
|
||||
content.appendChild(toggleSpacer)
|
||||
content.appendChild(text)
|
||||
treeCell.appendChild(indentLayer)
|
||||
treeCell.appendChild(content)
|
||||
TD.replaceChildren(treeCell)
|
||||
|
||||
return {
|
||||
treeCell,
|
||||
indentLayer,
|
||||
content,
|
||||
toggleEl: toggleSpacer,
|
||||
textEl: text,
|
||||
}
|
||||
}
|
||||
89
apps/web-ele/src/components/db-hst/validator.ts
Normal file
89
apps/web-ele/src/components/db-hst/validator.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// 验证行
|
||||
export const validatorRow = (_this: any,changes: any) => {
|
||||
// 获取列配置
|
||||
const columns = _this.getSettings().columns;
|
||||
|
||||
// 收集所有需要验证的行(去重)
|
||||
const rowsToValidate = new Set<number>();
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const currChange = changes[i];
|
||||
const row = currChange[0]; // 当前修改的行索引
|
||||
const prop = currChange[1]; // 当前修改的列(属性名)
|
||||
const oldValue = currChange[2]; // 修改前的值
|
||||
const newValue = currChange[3]; // 修改后的值
|
||||
|
||||
// console.log(`行${row}, 列${prop}, 从 ${oldValue} 改为 ${newValue}`)
|
||||
|
||||
// 将当前行加入待验证列表
|
||||
rowsToValidate.add(row);
|
||||
}
|
||||
let hasEmptyCell = false
|
||||
// 验证所有受影响的行
|
||||
for (const row of rowsToValidate) {
|
||||
// console.log(`验证第 ${row} 行的所有必填列`)
|
||||
|
||||
// 遍历所有列,验证 required: true 的列
|
||||
columns.forEach((columnConfig: any, colIndex: number) => {
|
||||
if (columnConfig.required === true) {
|
||||
// 获取当前单元格的值
|
||||
const cellValue = _this.getDataAtCell(row, colIndex);
|
||||
|
||||
// 检查值是否为空(null、undefined、空字符串)
|
||||
let isEmpty = false
|
||||
|
||||
// 对于 使用 db-dropdown renderer,需要特殊处理
|
||||
if (columnConfig.renderer === 'db-dropdown' || columnConfig.renderer === 'db-duplicate') {
|
||||
// 检查值是否为空或不在 source 列表中
|
||||
isEmpty = cellValue === null || cellValue === undefined || cellValue === ''
|
||||
|
||||
// 如果有值,还需要验证是否在允许的选项中
|
||||
if (!isEmpty && Array.isArray(columnConfig.source)) {
|
||||
const validValues = columnConfig.source.map((opt: any) =>
|
||||
typeof opt === 'object' && opt !== null ? opt.value : opt
|
||||
)
|
||||
isEmpty = !validValues.includes(cellValue)
|
||||
}
|
||||
} else {
|
||||
// 其他列的常规验证
|
||||
isEmpty = cellValue === null || cellValue === undefined || cellValue === ''
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
// 值为空,标记为无效
|
||||
_this.setCellMeta(row, colIndex, 'valid', false);
|
||||
hasEmptyCell = true
|
||||
// console.log(` 列 ${columnConfig.data} (索引${colIndex}) 值为空,标记为无效`);
|
||||
} else {
|
||||
// 对于 numeric 类型,额外验证是否为有效数字
|
||||
if (columnConfig.type === 'numeric') {
|
||||
const numValue = Number(cellValue)
|
||||
if (isNaN(numValue)) {
|
||||
// 不是有效数字,标记为无效
|
||||
_this.setCellMeta(row, colIndex, 'valid', false);
|
||||
hasEmptyCell = true
|
||||
// console.log(` 列 ${columnConfig.data} (索引${colIndex}) 不是有效数字,标记为无效`);
|
||||
} else {
|
||||
// 是有效数字,标记为有效
|
||||
_this.setCellMeta(row, colIndex, 'valid', true);
|
||||
// console.log(` 列 ${columnConfig.data} (索引${colIndex}) 是有效数字,标记为有效`);
|
||||
}
|
||||
} else {
|
||||
// 值不为空,标记为有效
|
||||
_this.setCellMeta(row, colIndex, 'valid', true);
|
||||
//console.log(` 列 ${columnConfig.data} (索引${colIndex}) 值不为空,标记为有效`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 重新渲染表格以显示验证状态
|
||||
_this.render();
|
||||
// 如果有空单元格,提前返回,不执行后续操作
|
||||
if(hasEmptyCell){
|
||||
console.log('存在空单元格,验证失败,不执行后续操作')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import type { DropdownInstance, TreeV2Instance } from 'element-plus'
|
||||
import { contextMenuManager } from '../db-hst/contextMenuManager'
|
||||
|
||||
interface NodeBase<T> { id: string; label: string; children?: T[] }
|
||||
type NodeType<T> = T & NodeBase<T>
|
||||
@@ -31,11 +32,19 @@ interface LevelConfig {
|
||||
addKey?: string
|
||||
addText?: string
|
||||
allowDelete?: boolean
|
||||
condition?: (node: any) => boolean // 添加条件判断函数,用于控制是否显示添加菜单
|
||||
sort?: string // 排序字段名,如 'sortOrder'
|
||||
customMenuItems?: Array<{ key: string; text: string; condition?: (node: any) => boolean }> // 自定义菜单项
|
||||
onAdd?: (parentNode: any, newNode: any, allChildren: any[]) => void | Promise<void>
|
||||
onDelete?: (node: any) => void | Promise<void>
|
||||
onCustomCommand?: (cmd: string, node: any) => void | Promise<void> // 自定义命令处理
|
||||
}
|
||||
|
||||
interface HierarchyConfig {
|
||||
rootKey: string
|
||||
rootText: string
|
||||
onRootAdd?: (newNode: any, allRootNodes: any[]) => void | Promise<void>
|
||||
onRootDelete?: (node: any) => void | Promise<void>
|
||||
levels: LevelConfig[]
|
||||
}
|
||||
|
||||
@@ -73,7 +82,9 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
||||
}
|
||||
|
||||
const depth = getDepth(node, ctx)
|
||||
console.log('getMenuItems - 节点:', node, '深度:', depth)
|
||||
const levelConfig = this.config.levels.find(l => l.depth === depth)
|
||||
console.log('找到的 levelConfig:', levelConfig)
|
||||
|
||||
if (!levelConfig) {
|
||||
// 未配置的层级,只显示删除
|
||||
@@ -82,9 +93,26 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
||||
|
||||
const items: MenuItem[] = []
|
||||
|
||||
// 添加子级菜单项
|
||||
// 添加子级菜单项(检查条件)
|
||||
if (levelConfig.addKey && levelConfig.addText) {
|
||||
items.push({ key: levelConfig.addKey, text: levelConfig.addText })
|
||||
console.log('检查 addKey 条件, levelConfig.condition:', levelConfig.condition)
|
||||
// 如果有条件函数,检查是否满足条件
|
||||
if (!levelConfig.condition || levelConfig.condition(node)) {
|
||||
console.log('条件满足,添加菜单项:', levelConfig.addText)
|
||||
items.push({ key: levelConfig.addKey, text: levelConfig.addText })
|
||||
} else {
|
||||
console.log('条件不满足,不添加菜单项')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义菜单项
|
||||
if (levelConfig.customMenuItems) {
|
||||
for (const customItem of levelConfig.customMenuItems) {
|
||||
// 如果有条件函数,检查是否满足条件
|
||||
if (!customItem.condition || customItem.condition(node)) {
|
||||
items.push({ key: customItem.key, text: customItem.text })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除菜单项
|
||||
@@ -100,7 +128,26 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
||||
if (!node && cmd === this.config.rootKey) {
|
||||
const next = ctx.createNode('root')
|
||||
next.label = this.config.rootText.replace('添加', '')
|
||||
|
||||
// 先添加到数据中
|
||||
ctx.setData([...ctx.dataRef.value, next])
|
||||
|
||||
// 调用根节点添加回调,传入所有根节点数据
|
||||
if (this.config.onRootAdd) {
|
||||
try {
|
||||
await this.config.onRootAdd(next, ctx.dataRef.value)
|
||||
// 回调完成后,重新设置数据以确保更新
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
} catch (error) {
|
||||
// 如果回调失败,移除刚添加的节点
|
||||
const index = ctx.dataRef.value.findIndex(n => n.id === next.id)
|
||||
if (index !== -1) {
|
||||
ctx.dataRef.value.splice(index, 1)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,29 +157,108 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
||||
if (cmd === 'delete') {
|
||||
const target = ctx.locate(node.id)
|
||||
if (!target) return
|
||||
|
||||
// 查找当前节点的层级配置
|
||||
const depth = getDepth(node, ctx)
|
||||
const levelConfig = this.config.levels.find(l => l.depth === depth)
|
||||
|
||||
// 如果是根节点(depth === -1 或 parent === null),调用根节点删除回调
|
||||
if (!target.parent && this.config.onRootDelete) {
|
||||
await this.config.onRootDelete(node)
|
||||
} else if (levelConfig?.onDelete) {
|
||||
// 否则调用层级删除回调
|
||||
await levelConfig.onDelete(node)
|
||||
}
|
||||
|
||||
target.container.splice(target.index, 1)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
return
|
||||
}
|
||||
|
||||
// 查找匹配的层级配置
|
||||
const depth = getDepth(node, ctx)
|
||||
const levelConfig = this.config.levels.find(l => l.depth === depth && l.addKey === cmd)
|
||||
|
||||
if (levelConfig) {
|
||||
const target = ctx.locate(node.id)
|
||||
if (!target) return
|
||||
// 查找匹配的层级配置
|
||||
const depth = getDepth(node, ctx)
|
||||
const levelConfig = this.config.levels.find(l => l.depth === depth && l.addKey === cmd)
|
||||
|
||||
const next = ctx.createNode(node.id)
|
||||
next.label = levelConfig.addText?.replace('添加', '') || '新目录'
|
||||
if (levelConfig) {
|
||||
const target = ctx.locate(node.id)
|
||||
if (!target) return
|
||||
|
||||
target.node.children = target.node.children || []
|
||||
|
||||
// 如果配置了排序字段,先对现有子节点排序
|
||||
if (levelConfig.sort) {
|
||||
const sortField = levelConfig.sort
|
||||
target.node.children.sort((a: any, b: any) => {
|
||||
const aVal = a[sortField] ?? 0
|
||||
const bVal = b[sortField] ?? 0
|
||||
return aVal - bVal
|
||||
})
|
||||
|
||||
// 计算新节点的排序号(取最大值 + 1)
|
||||
const maxSort = target.node.children.reduce((max: number, child: any) => {
|
||||
const childSort = child[sortField] ?? 0
|
||||
return Math.max(max, childSort)
|
||||
}, 0)
|
||||
|
||||
const next = ctx.createNode(node.id)
|
||||
next.label = levelConfig.addText?.replace('添加', '') || '新目录'
|
||||
;(next as any)[sortField] = maxSort + 1
|
||||
|
||||
target.node.children.push(next)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
ctx.expandNode(target.node.id)
|
||||
|
||||
// 调用添加回调,传递父节点的所有子节点
|
||||
if (levelConfig.onAdd) {
|
||||
try {
|
||||
await levelConfig.onAdd(node, next, target.node.children)
|
||||
// 回调完成后,重新设置数据以确保更新
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
} catch (error) {
|
||||
// 如果回调失败,移除刚添加的节点
|
||||
const index = target.node.children!.findIndex(n => n.id === next.id)
|
||||
if (index !== -1) {
|
||||
target.node.children!.splice(index, 1)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有配置排序字段,使用原逻辑
|
||||
const next = ctx.createNode(node.id)
|
||||
next.label = levelConfig.addText?.replace('添加', '') || '新目录'
|
||||
|
||||
target.node.children.push(next)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
ctx.expandNode(target.node.id)
|
||||
|
||||
// 调用添加回调,传递父节点的所有子节点
|
||||
if (levelConfig.onAdd) {
|
||||
try {
|
||||
await levelConfig.onAdd(node, next, target.node.children)
|
||||
// 回调完成后,重新设置数据以确保更新
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
} catch (error) {
|
||||
// 如果回调失败,移除刚添加的节点
|
||||
const index = target.node.children!.findIndex(n => n.id === next.id)
|
||||
if (index !== -1) {
|
||||
target.node.children!.splice(index, 1)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
target.node.children = target.node.children || []
|
||||
target.node.children.push(next)
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
ctx.expandNode(target.node.id)
|
||||
ctx.setCurrentKey(next.id)
|
||||
// 处理自定义命令
|
||||
const customLevelConfig = this.config.levels.find(l => l.depth === getDepth(node, ctx))
|
||||
if (customLevelConfig?.onCustomCommand) {
|
||||
await customLevelConfig.onCustomCommand(cmd, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +296,7 @@ class DefaultContextMenuHandler<T> implements ContextMenuHandler<T> {
|
||||
target.node.children.push(ctx.createNode(target.node.id))
|
||||
ctx.setData([...ctx.dataRef.value])
|
||||
ctx.expandNode(target.node.id)
|
||||
ctx.setCurrentKey(target.node.id)
|
||||
// ctx.setCurrentKey(target.node.id)
|
||||
return
|
||||
}
|
||||
if (cmd === 'rename') { ctx.startEdit(node); return }
|
||||
@@ -191,10 +317,22 @@ class DbTreeContextMenu<T> {
|
||||
|
||||
private config: ContextMenuConfig<T>
|
||||
private handler: ContextMenuHandler<T>
|
||||
private unregister: (() => void) | null = null
|
||||
|
||||
constructor(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T>) {
|
||||
this.config = config
|
||||
this.handler = handler ?? new DefaultContextMenuHandler<T>()
|
||||
|
||||
// 注册到全局菜单管理器
|
||||
this.unregister = contextMenuManager.register(this.closeContextMenu)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 取消注册
|
||||
if (this.unregister) {
|
||||
this.unregister()
|
||||
this.unregister = null
|
||||
}
|
||||
}
|
||||
|
||||
private createNode = (prefix: string): NodeType<T> => {
|
||||
@@ -223,18 +361,30 @@ class DbTreeContextMenu<T> {
|
||||
getMenuItems = (): MenuItem[] => this.handler.getMenuItems(this.currentNode.value, this.ctx())
|
||||
|
||||
openContextMenu = (event: MouseEvent, nodeData: NodeType<T>) => {
|
||||
const { clientX, clientY } = event
|
||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
||||
// console.log('openContextMenu',nodeData)
|
||||
// 通知管理器即将打开新菜单,关闭其他菜单
|
||||
contextMenuManager.notifyOpening(this.closeContextMenu)
|
||||
|
||||
event.preventDefault()
|
||||
const { clientX, clientY } = event
|
||||
this.currentNode.value = nodeData
|
||||
const items = this.handler.getMenuItems(this.currentNode.value, this.ctx())
|
||||
if (!items.length) return
|
||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
||||
this.dropdownRef.value?.handleOpen()
|
||||
}
|
||||
|
||||
openBlankContextMenu = (event: MouseEvent) => {
|
||||
const { clientX, clientY } = event
|
||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
||||
// console.log('openBlankContextMenu')
|
||||
// 通知管理器即将打开新菜单,关闭其他菜单
|
||||
contextMenuManager.notifyOpening(this.closeContextMenu)
|
||||
|
||||
event.preventDefault()
|
||||
const { clientX, clientY } = event
|
||||
this.currentNode.value = null
|
||||
const items = this.handler.getMenuItems(this.currentNode.value, this.ctx())
|
||||
if (!items.length) return
|
||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
||||
this.dropdownRef.value?.handleOpen()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
placeholder="请输入关键字"
|
||||
@input="onQueryChanged"
|
||||
/>
|
||||
<!-- @contextmenu="openBlankContextMenu" -->
|
||||
<ElTreeV2
|
||||
class="treeLine-2"
|
||||
:indent="0"
|
||||
ref="treeRef"
|
||||
style="max-width: 600px"
|
||||
|
||||
:data="data"
|
||||
:props="props"
|
||||
:filter-method="filterMethod"
|
||||
@@ -19,49 +20,64 @@
|
||||
:height="treeHeight"
|
||||
@node-expand="onNodeExpand"
|
||||
@node-collapse="onNodeCollapse"
|
||||
@contextmenu="openBlankContextMenu"
|
||||
@contextmenu="handleContextMenu"
|
||||
@nodeContextmenu="handleNodeContextMenu"
|
||||
highlight-current
|
||||
scrollbar-always-on
|
||||
:expand-on-click-node="false"
|
||||
@nodeClick="onNodeSingleClick"
|
||||
>
|
||||
<template #default="{ node, data: nodeData }">
|
||||
<!-- 根据层级生成占位符,level 1 不需要占位 -->
|
||||
<!-- :style="{ paddingLeft: node.isLeaf ? '0px' : '0px' }" -->
|
||||
<span v-for="i in (node.level - 1)" :key="i" class="node_nestingLevel_empty"></span>
|
||||
<div class="node-content-wrapper" :style="{ paddingLeft: node.isLeaf ? '0px' : '11px' }">
|
||||
<span class="node-icon-wrapper">
|
||||
<IconifyIcon
|
||||
v-if="!node.isLeaf"
|
||||
:icon="node.expanded ? 'ep:remove' : 'ep:circle-plus'"
|
||||
class="custom-expand-icon"
|
||||
/>
|
||||
</span>
|
||||
<template v-if="editingId === nodeData.id">
|
||||
<ElInput
|
||||
:id="`edit-${nodeData.id}`"
|
||||
v-model="editingLabel"
|
||||
@blur="saveEdit"
|
||||
@keydown.enter.prevent="saveEdit"
|
||||
@keydown.esc.prevent="cancelEdit"
|
||||
@click.stop
|
||||
size="small"
|
||||
class="node-edit-input"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
class="node-label"
|
||||
:style="getNodeStyles(nodeData)"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(nodeData, $event)"
|
||||
@dragover.prevent="onDragOver(nodeData, $event)"
|
||||
@drop.prevent="onDrop(nodeData, $event)"
|
||||
@dragend="onDragEnd"
|
||||
@contextmenu.stop="(e) => openContextMenu(e, nodeData)"
|
||||
@click="onNodeSingleClick(nodeData, $event)"
|
||||
@dblclick.stop="onNodeDblClick(nodeData, $event)"
|
||||
>
|
||||
{{ nodeData.label }}
|
||||
<div class="node-content-wrapper" :style="{ paddingLeft: '5px' }">
|
||||
<div class="node-left-content">
|
||||
<span class="node-icon-wrapper" >
|
||||
<IconifyIcon
|
||||
v-if="!node.isLeaf"
|
||||
:icon="node.expanded ? 'ep:remove' : 'ep:circle-plus'"
|
||||
class="custom-expand-icon"
|
||||
@click.stop="onNodeIconWrapperClick(node)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="editingId === nodeData.id">
|
||||
<ElInput
|
||||
:id="`edit-${nodeData.id}`"
|
||||
v-model="editingLabel"
|
||||
@blur="saveEdit"
|
||||
@keydown.enter.prevent="saveEdit"
|
||||
@keydown.esc.prevent="cancelEdit"
|
||||
@click.stop
|
||||
size="small"
|
||||
class="node-edit-input"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- @contextmenu.stop="(e) => openContextMenu(e, nodeData)" -->
|
||||
<span
|
||||
class="node-label"
|
||||
:style="getNodeStyles(nodeData)"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(nodeData, $event)"
|
||||
@dragover.prevent="onDragOver(nodeData, $event)"
|
||||
@drop.prevent="onDrop(nodeData, $event)"
|
||||
@dragend="onDragEnd"
|
||||
@dblclick.stop="onNodeDblClick(nodeData, $event)"
|
||||
>
|
||||
{{ nodeData.label }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 默认是type="info",有值时type="primary" -->
|
||||
<el-button
|
||||
v-if="node.isLeaf && shouldShowDecideButton(nodeData)"
|
||||
:type="nodeData.selected?'primary':'info'"
|
||||
circle
|
||||
size="small"
|
||||
:class="['decide-button', { 'decide-button-highlight': isDecideHighlighted(nodeData) }]"
|
||||
@click.stop="onDecideClick(nodeData, $event)"
|
||||
>{{ decideText }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</ElTreeV2>
|
||||
@@ -91,18 +107,27 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, computed, watch } from 'vue'
|
||||
import { ElTreeV2, ElDropdown, ElDropdownMenu, ElDropdownItem, ElInput } from 'element-plus'
|
||||
import type { TreeNodeData, TreeV2Instance } from 'element-plus'
|
||||
import { IconifyIcon } from '@vben/icons'
|
||||
import { useContextMenu } from './contextMenu'
|
||||
import type { TreeNodeData, TreeV2Instance } from 'element-plus'
|
||||
import { ElDropdown, ElDropdownItem, ElDropdownMenu, ElInput, ElTreeV2 } from 'element-plus'
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import type { ContextMenuHandler, HierarchyConfig } from './contextMenu'
|
||||
import { useInlineEdit } from './inlineEdit'
|
||||
import { useContextMenu } from './contextMenu'
|
||||
import { useDragAndDrop } from './draggable'
|
||||
|
||||
import { useInlineEdit } from './inlineEdit'
|
||||
defineOptions({ name: 'DbTree' });
|
||||
const componentProps = defineProps<{ height?: number; data?: Tree[]; defaultExpandedKeys?: number | string | string[]; contextMenuHandler?: ContextMenuHandler<Tree> | HierarchyConfig; search?: boolean }>()
|
||||
const emit = defineEmits<{ (e: 'select', node: Tree): void }>()
|
||||
const componentProps = defineProps<{
|
||||
height?: number;
|
||||
data?: Tree[];
|
||||
defaultExpandedKeys?: number | string | string[];
|
||||
contextMenuHandler?: ContextMenuHandler<Tree> | HierarchyConfig;
|
||||
search?: boolean;
|
||||
decide?: boolean;
|
||||
decideText?: string;
|
||||
decideHighlightKey?: string;
|
||||
decideFilter?: (node: Tree) => boolean; // 新增:过滤哪些节点可以显示工字按钮
|
||||
}>()
|
||||
const emit = defineEmits<{ (e: 'select', node: Tree): void; (e: 'edit-save', node: Tree, oldLabel: string, newLabel: string): void; (e: 'decide', node: Tree): void }>()
|
||||
|
||||
interface Tree {
|
||||
id: string
|
||||
@@ -122,6 +147,14 @@ watch(
|
||||
|
||||
const treeHeight = computed(() => componentProps.height ?? 400)
|
||||
const isSearchEnabled = computed(() => componentProps.search ?? true)
|
||||
const isDecideEnabled = computed(() => componentProps.decide ?? false)
|
||||
const decideText = computed(() => componentProps.decideText ?? '定')
|
||||
const decideHighlightKey = computed(() => componentProps.decideHighlightKey ?? '')
|
||||
const decideFilter = computed(() => componentProps.decideFilter ?? (() => true)) // 默认所有节点都可以显示
|
||||
const isContextMenuEnabled = computed(() => {
|
||||
// console.log(componentProps.contextMenuHandler)
|
||||
return componentProps.contextMenuHandler ?? false
|
||||
})
|
||||
const props = {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
@@ -173,6 +206,17 @@ const onQueryChanged = (query: string) => {
|
||||
const filterMethod = (query: string, node: TreeNodeData) =>
|
||||
node.label!.includes(query)
|
||||
|
||||
const isDecideHighlighted = (nodeData: Tree) => {
|
||||
const key = decideHighlightKey.value
|
||||
if (!key) return false
|
||||
return Boolean((nodeData as Record<string, any>)[key])
|
||||
}
|
||||
|
||||
const shouldShowDecideButton = (nodeData: Tree) => {
|
||||
if (!isDecideEnabled.value) return false
|
||||
return decideFilter.value(nodeData)
|
||||
}
|
||||
|
||||
type LocateResult = { node: Tree; parent: Tree | null; container: Tree[]; index: number }
|
||||
|
||||
const locate = (id: string): LocateResult | null => {
|
||||
@@ -187,36 +231,97 @@ const locate = (id: string): LocateResult | null => {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const { editingId, editingLabel, editingOriginalLabel, startEdit, saveEdit, cancelEdit } = useInlineEdit<Tree>({ dataRef: data, locate })
|
||||
type TreeKey = string | number
|
||||
interface TreeNode {
|
||||
key: TreeKey
|
||||
level: number
|
||||
parent?: TreeNode
|
||||
children?: TreeNode[]
|
||||
data: TreeNodeData
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
isLeaf?: boolean
|
||||
expanded?: boolean
|
||||
isEffectivelyChecked?: boolean
|
||||
}
|
||||
const onNodeIconWrapperClick = (node: TreeNode) => {
|
||||
if (!treeRef.value) return
|
||||
if (node.isLeaf) return
|
||||
if (node.expanded) treeRef.value.collapseNode(node)
|
||||
else treeRef.value.expandNode(node)
|
||||
}
|
||||
const handleEditSave = async (node: Tree, oldLabel: string, newLabel: string) => {
|
||||
emit('edit-save', node, oldLabel, newLabel)
|
||||
}
|
||||
const { editingId, editingLabel, editingOriginalLabel, startEdit, saveEdit, cancelEdit } = useInlineEdit<Tree>({ dataRef: data, locate, onSave: handleEditSave })
|
||||
const { dropdownRef, position, triggerRef, currentNode, openContextMenu, openBlankContextMenu, closeContextMenu, onGlobalCommand, getMenuItems } = useContextMenu<Tree>({ dataRef: data, treeRef, expandedKeysRef: expandedKeys, locate, startEdit }, componentProps.contextMenuHandler)
|
||||
const { draggingId, dropState, getNodeStyles, onDragStart, onDragOver, onDrop, onDragEnd } = useDragAndDrop<Tree>({ dataRef: data, treeRef, expandedKeysRef: expandedKeys, locate })
|
||||
|
||||
// 包装右键菜单处理函数,根据 isContextMenuEnabled 决定是否执行
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
// 始终阻止浏览器默认右键菜单
|
||||
e.preventDefault()
|
||||
|
||||
if (isContextMenuEnabled.value) {
|
||||
openBlankContextMenu(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeContextMenu = (e: MouseEvent, data: Tree) => {
|
||||
// 始终阻止浏览器默认右键菜单
|
||||
e.preventDefault()
|
||||
|
||||
if (isContextMenuEnabled.value) {
|
||||
openContextMenu(e, data)
|
||||
}
|
||||
}
|
||||
|
||||
//防止误触发
|
||||
let clickTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const clickDelayMs = 250
|
||||
const triggerSingleClick = (node: Tree) => {
|
||||
closeContextMenu()
|
||||
emit('select', node)
|
||||
}
|
||||
|
||||
const onNodeSingleClick = (node: Tree, e: MouseEvent) => {
|
||||
// 清除之前的定时器
|
||||
if (clickTimer) {
|
||||
clearTimeout(clickTimer)
|
||||
clickTimer = null
|
||||
}
|
||||
|
||||
// 单击延迟执行,避免与双击冲突
|
||||
clickTimer = setTimeout(() => {
|
||||
triggerSingleClick(node)
|
||||
closeContextMenu()
|
||||
emit('select', node)
|
||||
clickTimer = null
|
||||
}, clickDelayMs)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const onNodeDblClick = (node: Tree, e: MouseEvent) => {
|
||||
console.log('onNodeDblClick')
|
||||
|
||||
// 清除单击定时器,避免触发单击事件
|
||||
if (clickTimer) {
|
||||
clearTimeout(clickTimer)
|
||||
clickTimer = null
|
||||
}
|
||||
|
||||
// 直接启动编辑,不需要防抖
|
||||
startEdit(node)
|
||||
}
|
||||
|
||||
const onDecideClick = (node: Tree, e: MouseEvent) => {
|
||||
// 阻止事件冒泡和默认行为
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
// 清除可能存在的单击定时器
|
||||
if (clickTimer) {
|
||||
clearTimeout(clickTimer)
|
||||
clickTimer = null
|
||||
}
|
||||
|
||||
// 触发 decide 事件
|
||||
emit('decide', node)
|
||||
}
|
||||
|
||||
const onNodeExpand = (data: TreeNodeData) => {
|
||||
const key = (data as any)[props.value] as string
|
||||
if (!key) return
|
||||
@@ -233,6 +338,25 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
const setCurrentKey = (key: string | number | null) => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCurrentKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setCurrentKey
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
// 如果 contextMenu 实例有 destroy 方法,调用它
|
||||
if (typeof (dropdownRef.value as any)?.destroy === 'function') {
|
||||
(dropdownRef.value as any).destroy()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -244,9 +368,23 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
||||
// gap: 8px;
|
||||
}
|
||||
.treeLine-2 {
|
||||
:deep(.el-tree__empty-block) {
|
||||
height: v-bind('treeHeight + "px"');
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
// 设置颜色
|
||||
background-color: #e9ecfc !important; // 透明度为0.2的skyblue,作者比较喜欢的颜色
|
||||
color: #273fe2; // 节点的字体颜色
|
||||
// font-weight: bold; // 字体加粗
|
||||
box-shadow: inset 2px 0 0 0 #0000ff !important;
|
||||
}
|
||||
// :deep(.el-tree .el-tree-node.is-current > .el-tree-node__content) {
|
||||
// box-shadow: inset 2px 0 0 0 var(--992e623a);
|
||||
// }
|
||||
.node_nestingLevel_empty {
|
||||
display: inline-block;
|
||||
padding-left: 18px;
|
||||
padding-left: 13px;
|
||||
}
|
||||
.node_nestingLevel_empty::before {
|
||||
content: '';
|
||||
@@ -271,6 +409,16 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
||||
align-items: center;
|
||||
gap: 0px;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.node-left-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.node-icon-wrapper {
|
||||
@@ -303,19 +451,34 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.decide-button {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
padding: 0 !important;
|
||||
font-size: 12px !important;
|
||||
min-width: 20px !important;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.decide-button-highlight {
|
||||
background-color: #cfe8ff !important;
|
||||
border-color: #cfe8ff !important;
|
||||
color: #1f3b66 !important;
|
||||
}
|
||||
/* 选择后面还有 ht_nestingLevel_empty 的元素(不是最后一个) */
|
||||
.node_nestingLevel_empty:has(+ .node_nestingLevel_empty)::after {
|
||||
/* 你的样式 */
|
||||
width: 0px;
|
||||
}
|
||||
:deep(.el-tree-node){
|
||||
left: -9px !important;
|
||||
// left: -9px !important;
|
||||
|
||||
}
|
||||
:deep(.el-tree-node__expand-icon){
|
||||
display: none !important;
|
||||
}
|
||||
// :deep(.el-tree-node__content){
|
||||
// display: none !important;
|
||||
// }
|
||||
:deep(.el-tree-node .el-tree-node__content){
|
||||
// padding-left: 5px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,8 +7,9 @@ type LocateResult<T> = { node: NodeType<T>; parent: NodeType<T> | null; containe
|
||||
export const useInlineEdit = <T>(params: {
|
||||
dataRef: Ref<NodeType<T>[]>;
|
||||
locate: (id: string) => LocateResult<T>;
|
||||
onSave?: (node: NodeType<T>, oldLabel: string, newLabel: string) => void | Promise<void>;
|
||||
}) => {
|
||||
const { dataRef, locate } = params
|
||||
const { dataRef, locate, onSave } = params
|
||||
const editingId = ref<string | null>(null)
|
||||
const editingLabel = ref('')
|
||||
const editingOriginalLabel = ref('')
|
||||
@@ -24,12 +25,18 @@ export const useInlineEdit = <T>(params: {
|
||||
})
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
const saveEdit = async () => {
|
||||
if (!editingId.value) return
|
||||
const target = locate(editingId.value)
|
||||
if (!target) { editingId.value = null; return }
|
||||
const next = editingLabel.value.trim()
|
||||
if (next) target.node.label = next
|
||||
const oldLabel = editingOriginalLabel.value
|
||||
if (next && next !== oldLabel) {
|
||||
target.node.label = next
|
||||
if (onSave) {
|
||||
await onSave(target.node, oldLabel, next)
|
||||
}
|
||||
}
|
||||
dataRef.value = [...dataRef.value]
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
18
apps/web-ele/src/router/routes/modules/database.ts
Normal file
18
apps/web-ele/src/router/routes/modules/database.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/database/entry/add',
|
||||
component: () => import('#/views/database/entry/add.vue'),
|
||||
name: 'EntryAdd',
|
||||
meta: {
|
||||
title: '信息价录入',
|
||||
icon: 'ant-design:history-outlined',
|
||||
activePath: '/database/entry',
|
||||
keepAlive: false,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
182
apps/web-ele/src/views/database/entry/add.ts
Normal file
182
apps/web-ele/src/views/database/entry/add.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { selectCellBorderStyle } from '#/components/db-hst/base'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
import Handsontable from 'handsontable'
|
||||
export const createUploadPhotoRender = (callbacks: {
|
||||
onPreview: (value: string) => void;
|
||||
onUpload: (row: number, col: number, value: string) => void;
|
||||
}) => {
|
||||
return (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
td.innerHTML = '';
|
||||
|
||||
// 创建 flex 容器
|
||||
const container = document.createElement('div');
|
||||
container.style.display = 'flex';
|
||||
container.style.alignItems = 'center';
|
||||
container.style.justifyContent = 'space-between';
|
||||
container.style.gap = '4px';
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
|
||||
// 左侧图片容器
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.style.flex = '1';
|
||||
imgContainer.style.display = 'flex';
|
||||
imgContainer.style.alignItems = 'center';
|
||||
imgContainer.style.overflow = 'hidden';
|
||||
|
||||
if (value) {
|
||||
const img = document.createElement('img');
|
||||
img.src = value
|
||||
img.style.maxWidth = '25px';
|
||||
img.style.maxHeight = '25px';
|
||||
img.style.objectFit = 'contain';
|
||||
img.style.cursor = 'pointer';
|
||||
|
||||
// 点击图片预览
|
||||
img.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
callbacks.onPreview(value);
|
||||
});
|
||||
|
||||
img.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
imgContainer.appendChild(img);
|
||||
}
|
||||
|
||||
//右侧上传图标
|
||||
const uploadIcon = document.createElement('span');
|
||||
uploadIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M11 16V7.85l-2.6 2.6L7 9l5-5l5 5l-1.4 1.45l-2.6-2.6V16zm-5 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"/></svg>';
|
||||
uploadIcon.style.cursor = 'pointer';
|
||||
uploadIcon.style.color = '#979797';
|
||||
uploadIcon.style.display = 'flex';
|
||||
uploadIcon.style.alignItems = 'center';
|
||||
uploadIcon.style.flexShrink = '0';
|
||||
|
||||
uploadIcon.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
callbacks.onUpload(row, col, value || '');
|
||||
});
|
||||
|
||||
uploadIcon.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
container.appendChild(imgContainer);
|
||||
container.appendChild(uploadIcon);
|
||||
td.appendChild(container);
|
||||
|
||||
return td;
|
||||
};
|
||||
}
|
||||
|
||||
export const createMorePriceRender = (callbacks: {
|
||||
onSelectPrice: (row: number) => void;
|
||||
}) => {
|
||||
return (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
td.innerHTML = '';
|
||||
td.style.padding = '2px';
|
||||
td.style.display = 'flex';
|
||||
td.style.alignItems = 'center';
|
||||
td.style.justifyContent = 'space-between';
|
||||
td.style.gap = '4px';
|
||||
|
||||
// 左侧价格文本
|
||||
const priceText = document.createElement('span');
|
||||
priceText.textContent = value || '';
|
||||
priceText.style.flex = '1';
|
||||
priceText.style.overflow = 'hidden';
|
||||
priceText.style.textOverflow = 'ellipsis';
|
||||
priceText.style.whiteSpace = 'nowrap';
|
||||
|
||||
// 右侧选择图标
|
||||
const selectIcon = document.createElement('span');
|
||||
selectIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14zM7 10h2v7H7zm4-3h2v10h-2zm4 6h2v4h-2z"/></svg>';
|
||||
selectIcon.style.cursor = 'pointer';
|
||||
selectIcon.style.color = '#979797';
|
||||
selectIcon.style.display = 'flex';
|
||||
selectIcon.style.alignItems = 'center';
|
||||
selectIcon.style.flexShrink = '0';
|
||||
|
||||
selectIcon.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
callbacks.onSelectPrice(row);
|
||||
});
|
||||
|
||||
selectIcon.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
td.appendChild(priceText);
|
||||
td.appendChild(selectIcon);
|
||||
|
||||
return td;
|
||||
};
|
||||
}
|
||||
|
||||
export const createIdCellRenderer = (callbacks: {
|
||||
onShowMorePrice: (row: number, col: number, value: any) => void;
|
||||
}) => {
|
||||
return (
|
||||
instance: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: string | number,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
Handsontable.renderers.TextRenderer(instance, TD, row, col, prop, value, cellProperties)
|
||||
TD.classList.add('ht-id-cell')
|
||||
// TD.style.textAlign = 'center'
|
||||
|
||||
const existingIcon = TD.querySelector('.ht-id-icon') as HTMLElement | null
|
||||
if (existingIcon) existingIcon.remove()
|
||||
|
||||
const iconButton = document.createElement('div')
|
||||
iconButton.className = 'ht-id-icon'
|
||||
iconButton.title = '查看历史价格'
|
||||
iconButton.innerHTML = '<svg t="1766623587261" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13644" width="16" height="16"><path d="M981.25962 291.954992a476.085215 476.085215 0 0 0-110.062711-152.296077 511.919586 511.919586 0 0 0-160.828069-102.383917 546.474158 546.474158 0 0 0-396.73768 0 511.919586 511.919586 0 0 0-162.534468 102.383917A478.218213 478.218213 0 0 0 42.74038 291.954992a451.769035 451.769035 0 0 0-42.659966 187.277249 461.154227 461.154227 0 0 0 127.979897 316.110344l23.036381 196.662441a36.260971 36.260971 0 0 0 35.834371 31.994974 36.260971 36.260971 0 0 0 17.917185-4.692596l148.45668-85.319931a547.327357 547.327357 0 0 0 358.343711-13.651189 511.919586 511.919586 0 0 0 162.534468-102.383917A478.218213 478.218213 0 0 0 981.25962 666.936089a453.902033 453.902033 0 0 0 0-374.981097z m-691.091441 243.161803a55.031355 55.031355 0 1 1 55.031356-55.031355 55.031355 55.031355 0 0 1-53.324957 55.031355z m220.125422 0a55.031355 55.031355 0 1 1 55.031356-55.031355A55.031355 55.031355 0 0 1 512 535.116795z m220.125422 0a55.031355 55.031355 0 1 1 55.031356-55.031355 55.031355 55.031355 0 0 1-53.751557 55.031355z" fill="#1a42e8" p-id="13645"></path></svg>'
|
||||
|
||||
iconButton.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
callbacks.onShowMorePrice(row, col, value)
|
||||
})
|
||||
|
||||
iconButton.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
TD.appendChild(iconButton)
|
||||
}
|
||||
}
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
export const moreContextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
638
apps/web-ele/src/views/database/entry/add.vue
Normal file
638
apps/web-ele/src/views/database/entry/add.vue
Normal file
@@ -0,0 +1,638 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed, readonly, } from 'vue'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElSplitter,ElSplitterPanel,ElInput,ElCard, ElMessage, ElImage,ElPopover, ElDatePickerPanel,ElText } from 'element-plus';
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import HistoryDialog from './components/HistoryDialog.vue'
|
||||
import { createDateRangeRenderer } from './index'
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { validatorRow } from '#/components/db-hst/validator'
|
||||
import { createUploadPhotoRender, createMorePriceRender, createIdCellRenderer, contextMenuItems, moreContextMenuItems } from './add'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
// import { sourceDataObject } from '#/components/db-hst/mockData'
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const { height: containerHeight } = useElementSize(containerRef)
|
||||
// const topContainerRef = ref<HTMLElement | null>(null)
|
||||
// const { height: topContainerHeight } = useElementSize(topContainerRef)
|
||||
const hstContainerRef = ref<HTMLElement | null>(null)
|
||||
const { height: hstContainerHeight } = useElementSize(hstContainerRef)
|
||||
|
||||
type Tree = { id: string; label: string; children?: Tree[] }
|
||||
const categoryTreeData = ref<Tree[]>([
|
||||
{
|
||||
id: '1',
|
||||
label: '行业总类',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
label: '广东',
|
||||
children: [
|
||||
{ id: '3', label: '行业1' },
|
||||
{ id: '4', label: '行业2' },
|
||||
{ id: '5', label: '行业3' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '行业2',
|
||||
children: [
|
||||
{
|
||||
id: '12',
|
||||
label: '广西',
|
||||
children: [
|
||||
{ id: '13', label: '行业5' },
|
||||
{ id: '14', label: '行业6' },
|
||||
{ id: '15', label: '行业7' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
// const colHeaders = ref<string[]>(topColHeaders)
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 创建回调函数
|
||||
const handlePhotoPreview = (value: string) => {
|
||||
previewImageUrl.value = value
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const handlePhotoUpload = (row: number, col: number, value: string) => {
|
||||
currentUploadRow.value = row
|
||||
currentUploadCol.value = col
|
||||
uploadImageUrl.value = value
|
||||
uploadDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handlePriceSelect = (row: number) => {
|
||||
currentPriceRow.value = row
|
||||
morePriceDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleShowMorePrice = (row: number, col: number, value: any) => {
|
||||
currentPriceRow.value = row
|
||||
morePriceDialogVisible.value = true
|
||||
console.log('查看历史价格:', { row, col, value })
|
||||
}
|
||||
const handleMorePriceDialogClose = () => {
|
||||
morePriceDialogVisible.value = false
|
||||
}
|
||||
const handleSelectPrice = (row: any) => {
|
||||
// 选择价格后更新表格数据
|
||||
if (hstRef.value?.hotInstance) {
|
||||
const colIndex = hstRef.value.hotInstance.propToCol('priceExcludeTax')
|
||||
hstRef.value.hotInstance.setDataAtCell(currentPriceRow.value, colIndex, row.priceExcludeTax)
|
||||
|
||||
const taxRateIndex = hstRef.value.hotInstance.propToCol('taxRate')
|
||||
hstRef.value.hotInstance.setDataAtCell(currentPriceRow.value, taxRateIndex, row.taxRate)
|
||||
|
||||
const priceIncludeTaxIndex = hstRef.value.hotInstance.propToCol('priceIncludeTax')
|
||||
hstRef.value.hotInstance.setDataAtCell(currentPriceRow.value, priceIncludeTaxIndex, row.priceIncludeTax)
|
||||
}
|
||||
|
||||
ElMessage.success('价格已更新')
|
||||
handleMorePriceDialogClose()
|
||||
}
|
||||
// 上传处理
|
||||
const handleUploadSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
|
||||
// 这里假设上传成功后返回图片URL
|
||||
// 实际项目中需要根据后端返回的数据结构调整
|
||||
const imageUrl = URL.createObjectURL(uploadFile.raw!)
|
||||
uploadImageUrl.value = imageUrl
|
||||
|
||||
// 更新表格数据
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.setDataAtCell(currentUploadRow.value, currentUploadCol.value, imageUrl)
|
||||
}
|
||||
}
|
||||
const handleHistoryConfirm = (node: any) => {
|
||||
console.log('确认选择的历史节点:', node)
|
||||
// 这里可以根据选中的节点加载对应的表格数据
|
||||
}
|
||||
|
||||
// 更多价格对话框相关
|
||||
const morePriceDialogVisible = ref(false)
|
||||
const currentPriceRow = ref<number>(0)
|
||||
const morePriceTableData = ref<any[]>([
|
||||
{
|
||||
id: 1,
|
||||
priceDate: '2024-01',
|
||||
priceExcludeTax: '1250.00',
|
||||
taxRate: '13',
|
||||
priceIncludeTax: '1412.50'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
priceDate: '2024-02',
|
||||
priceExcludeTax: '1280.00',
|
||||
taxRate: '13',
|
||||
priceIncludeTax: '1446.40'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
priceDate: '2024-03',
|
||||
priceExcludeTax: '1300.00',
|
||||
taxRate: '13',
|
||||
priceIncludeTax: '1469.00'
|
||||
}
|
||||
])
|
||||
|
||||
// 历史信息对话框相关
|
||||
const historyDialogVisible = ref(false)
|
||||
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{type:'text',data:'number',title:'序号'},
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'model',title:'型号规格'},
|
||||
{type:'text',data:'unit',title:'单位',width: 50, className: 'htCenter'},
|
||||
{type:'numeric',data:'priceExcludeTax',title:'除税编制价'},
|
||||
{type:'numeric',data:'taxRate',title:'税率%',width: 60, renderer: createIdCellRenderer({
|
||||
onShowMorePrice: handleShowMorePrice
|
||||
}), className: 'ht-id-cell htCenter'},
|
||||
{type:'numeric',data:'priceIncludeTax',title:'含税编制价'},
|
||||
{type:'text',data:'drawing',title:'图样(仅供参考)',readOnly: true,
|
||||
renderer: createUploadPhotoRender({
|
||||
onPreview: handlePhotoPreview,
|
||||
onUpload: handlePhotoUpload
|
||||
})},
|
||||
{type:'text',data:'category',title:'分类',renderer: 'db-dropdown',
|
||||
source: ['措施类1', '措施类2', '措施类3', '措施类3'],
|
||||
readOnly: true,},
|
||||
// {type:'text',data:'servicePrice',title:'劳务单价',renderer: createMorePriceRender({
|
||||
// onSelectPrice: handlePriceSelect
|
||||
// })},
|
||||
{type:'text',data:'remark',title:'备注'},
|
||||
// {type:'text',data:'',title:'操作'},
|
||||
])
|
||||
// const colHeaders = ref<string[]>(topColHeaders)
|
||||
|
||||
|
||||
const mockData = ()=>{
|
||||
// 生成模拟数据
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `DTL${String(index + 1).padStart(6, '0')}`,
|
||||
name: `明细项目${index + 1}`,
|
||||
model: `型号${index + 1}-规格${index + 1}`,
|
||||
unit: ['个', '米', '吨', '套', '台'][index % 5],
|
||||
priceExcludeTax: (Math.random() * 1000 + 100).toFixed(2),
|
||||
taxRate: ['13', '9', '6', '3'][index % 4],
|
||||
priceIncludeTax: (Math.random() * 1200 + 120).toFixed(2),
|
||||
drawing: ``,
|
||||
category: ['材料', '设备', '人工', '机械'][index % 4],
|
||||
servicePrice: (Math.random() * 500 + 50).toFixed(2),
|
||||
remark: `备注${index + 1}`,
|
||||
}))
|
||||
return mockData;
|
||||
}
|
||||
let dbSettings = {
|
||||
data:mockData(),
|
||||
columns: columns.value,
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {
|
||||
name: '在上方插入行',
|
||||
// callback: function(key: string, selection: any[]) {
|
||||
// const selectedRow = selection[0].start.row
|
||||
// this.alter('insert_row_above', selectedRow, 1)
|
||||
// },
|
||||
},
|
||||
row_below: {
|
||||
name: '在下方插入行',
|
||||
// callback: function(key: string, selection: any[]) {
|
||||
// const selectedRow = selection[0].start.row
|
||||
// this.alter('insert_row_below', selectedRow, 1)
|
||||
// },
|
||||
},
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
// 如果有 ID,可以调用删除 API
|
||||
// deleteResourceMerged(rowData.id).then(res => {
|
||||
// console.log('deleteResourceMerged',res)
|
||||
// })
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
afterChange(changes, source) {
|
||||
console.log('source',source)
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return
|
||||
|
||||
// 如果有空单元格,提前返回,不执行后续操作
|
||||
if(!validatorRow(this, changes)){
|
||||
return
|
||||
}
|
||||
//console.log('currChange', currChange)
|
||||
const row = changes[0][0]; // 获取行索引
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// 构建更新数据
|
||||
const sendData: any = {
|
||||
// catalogItemId: catalogItemId.value,
|
||||
// categoryId: categoryId,
|
||||
...rowData
|
||||
}
|
||||
|
||||
if (rowData.id == null) {
|
||||
console.log('moreDbSettings-create', sendData )
|
||||
// 调用创建接口
|
||||
// createResourceItems(sendData).then(res => {
|
||||
// console.log('createResourceItems', res)
|
||||
// // 更新当前行ID - 使用自定义source避免循环
|
||||
// this.setDataAtRowProp(row, 'id', res, 'updateId');
|
||||
// ElMessage.success('新增成功')
|
||||
// }).catch(err => {
|
||||
// console.error('新增失败', err)
|
||||
// // ElMessage.error('新增失败')
|
||||
// })
|
||||
} else {
|
||||
// 调用更新接口
|
||||
console.log('moreDbSettings-update', sendData)
|
||||
// 实现更新接口调用
|
||||
// updateResourceItems(sendData).then(res => {
|
||||
// console.log('updateResourceItems', res)
|
||||
// }).catch(err => {
|
||||
// console.error('更新失败', err)
|
||||
// // ElMessage.error('更新失败')
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传对话框相关
|
||||
const uploadDialogVisible = ref(false)
|
||||
const currentUploadRow = ref<number>(0)
|
||||
const currentUploadCol = ref<number>(0)
|
||||
const uploadImageUrl = ref<string>('')
|
||||
// 图片预览相关
|
||||
const previewImageUrl = ref<string>('')
|
||||
const previewVisible = ref(false)
|
||||
const handleBeforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
if (!rawFile.type.startsWith('image/')) {
|
||||
console.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (rawFile.size / 1024 / 1024 > 5) {
|
||||
console.error('图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUploadChange: UploadProps['onChange'] = (uploadFile) => {
|
||||
if (uploadFile.raw) {
|
||||
const imageUrl = URL.createObjectURL(uploadFile.raw)
|
||||
uploadImageUrl.value = imageUrl
|
||||
|
||||
// 更新表格数据
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.setDataAtCell(currentUploadRow.value, currentUploadCol.value, imageUrl)
|
||||
}
|
||||
handleDialogClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
uploadImageUrl.value = ''
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.setDataAtCell(currentUploadRow.value, currentUploadCol.value, '')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
uploadDialogVisible.value = false
|
||||
}
|
||||
|
||||
const handlePreviewClose = () => {
|
||||
previewVisible.value = false
|
||||
previewImageUrl.value = ''
|
||||
}
|
||||
|
||||
|
||||
const categoryHandleSelect = (node: Tree) => {
|
||||
console.log('categoryhandleSelect',node)
|
||||
}
|
||||
const detailHandleSelect = (node: Tree) => {
|
||||
// if (topHstRef.value && typeof topHstRef.value.loadData === 'function') {
|
||||
// // console.log('hstData.value',hstData.value)
|
||||
// // topHstRef.value.loadData(topHstData.value)
|
||||
// }
|
||||
}
|
||||
const search = ref()
|
||||
const handleQuery = () => {
|
||||
console.log('handleQuery')
|
||||
}
|
||||
|
||||
const moreHstRef = ref()
|
||||
// Popover 相关
|
||||
const popoverDateRangeRef = ref()
|
||||
const visibleDateRange = ref(false)
|
||||
const popoverDate = ref<[Date, Date]>()
|
||||
const currentEditingRow = ref()
|
||||
const handleCalendarChange = (val: any) =>{
|
||||
nextTick(()=>{
|
||||
console.log('handleCalendarChange',val, popoverDate.value)
|
||||
if(val[0] != null && val[1] != null){
|
||||
visibleDateRange.value = false
|
||||
setTimeout(() => {
|
||||
// 更新当前行的数据
|
||||
if (currentEditingRow.value >= 0 && popoverDate.value && hstRef.value?.hotInstance) {
|
||||
const [startDate, endDate] = popoverDate.value
|
||||
const dateRangeStr = `${startDate} ~ ${endDate}`
|
||||
|
||||
// 更新 Handsontable 中的数据
|
||||
moreHstRef.value.hotInstance.setDataAtRowProp(currentEditingRow.value, 'time', dateRangeStr)
|
||||
|
||||
console.log(`已更新第 ${currentEditingRow.value} 行的价格时间段为: ${dateRangeStr}`)
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
}
|
||||
// 创建更多价格时间段渲染器
|
||||
const dateRangeRenderer = createDateRangeRenderer({
|
||||
onDateIconClick: (row: number, data: any, event: MouseEvent) => {
|
||||
console.log('时间图标点击:', data)
|
||||
// 先关闭 popover
|
||||
visibleDateRange.value = false
|
||||
// 使用 nextTick 确保 DOM 更新后再打开
|
||||
nextTick(() => {
|
||||
currentEditingRow.value = row
|
||||
popoverDateRangeRef.value = event.target
|
||||
visibleDateRange.value = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const moreColumns = ref<any[]>([
|
||||
{type:'text',data:'time',title:'价格时间段',width: 200, renderer: dateRangeRenderer},
|
||||
{type:'numeric',data:'priceExcludeTax',title:'除税单价'},
|
||||
{type:'numeric',data:'taxRate',title:'税率%',width: 60},
|
||||
{type:'numeric',data:'priceIncludeTax',title:'含税单价'},
|
||||
])
|
||||
let moreDbSettings = {
|
||||
data: [],
|
||||
columns: moreColumns.value,
|
||||
height: 300,
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {
|
||||
name: '在上方插入行',
|
||||
// callback: function(key: string, selection: any[]) {
|
||||
// const selectedRow = selection[0].start.row
|
||||
// this.alter('insert_row_above', selectedRow, 1)
|
||||
// },
|
||||
},
|
||||
row_below: {
|
||||
name: '在下方插入行',
|
||||
// callback: function(key: string, selection: any[]) {
|
||||
// const selectedRow = selection[0].start.row
|
||||
// this.alter('insert_row_below', selectedRow, 1)
|
||||
// },
|
||||
},
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
// 如果有 ID,可以调用删除 API
|
||||
// deleteResourceMerged(rowData.id).then(res => {
|
||||
// console.log('deleteResourceMerged',res)
|
||||
// })
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
afterChange(changes, source) {
|
||||
console.log('source',source)
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return
|
||||
|
||||
// 如果有空单元格,提前返回,不执行后续操作
|
||||
if(!validatorRow(this, changes)){
|
||||
return
|
||||
}
|
||||
//console.log('currChange', currChange)
|
||||
const row = changes[0][0]; // 获取行索引
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// 构建更新数据
|
||||
const sendData: any = {
|
||||
// catalogItemId: catalogItemId.value,
|
||||
// categoryId: categoryId,
|
||||
...rowData
|
||||
}
|
||||
|
||||
if (rowData.id == null) {
|
||||
console.log('moreDbSettings-create', sendData )
|
||||
// 调用创建接口
|
||||
// createResourceItems(sendData).then(res => {
|
||||
// console.log('createResourceItems', res)
|
||||
// // 更新当前行ID - 使用自定义source避免循环
|
||||
// this.setDataAtRowProp(row, 'id', res, 'updateId');
|
||||
// ElMessage.success('新增成功')
|
||||
// }).catch(err => {
|
||||
// console.error('新增失败', err)
|
||||
// // ElMessage.error('新增失败')
|
||||
// })
|
||||
} else {
|
||||
// 调用更新接口
|
||||
console.log('moreDbSettings-update', sendData)
|
||||
// 实现更新接口调用
|
||||
// updateResourceItems(sendData).then(res => {
|
||||
// console.log('updateResourceItems', res)
|
||||
// }).catch(err => {
|
||||
// console.error('更新失败', err)
|
||||
// // ElMessage.error('更新失败')
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onBottomHeight(height: number){
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: height
|
||||
})
|
||||
|
||||
// hstRef.value.loadData(bootomMock())
|
||||
hstRef.value.hotInstance.render()
|
||||
console.log('onResizeEnd-hstRef',height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
onBottomHeight(hstContainerHeight.value)
|
||||
}, 200);
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElCard class="w-full border-radius-0" body-class="!p-0" header-class="!p-1">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<ElText class="mx-1">信息价名称: 珠海市</ElText>
|
||||
<!-- <span>信息价名称: 珠海市</span> -->
|
||||
<ElInput
|
||||
v-model="search"
|
||||
style="width: 300px"
|
||||
placeholder="请输入"
|
||||
class="input-with-select"
|
||||
size="small"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton @click="handleQuery" size="small"><IconifyIcon icon="ep:search"/></ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton @click="historyDialogVisible = true" type="primary" size="small" style="float: right;">调用历史信息</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElCard>
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="categoryTreeData" @select="categoryHandleSelect" :defaultExpandedKeys="2" :search="false" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="hstContainerRef">
|
||||
<DbHst ref="hstRef" :settings="dbSettings" :contextMenuItems="contextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
<!-- 历史信息对话框 -->
|
||||
<HistoryDialog
|
||||
v-model:visible="historyDialogVisible"
|
||||
@confirm="handleHistoryConfirm"
|
||||
/>
|
||||
<!-- 上传图片对话框 -->
|
||||
<ElDialog
|
||||
v-model="uploadDialogVisible"
|
||||
title="上传图片"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<div class="upload-container">
|
||||
<!-- <div v-if="uploadImageUrl" class="image-preview">
|
||||
<img :src="uploadImageUrl" alt="预览图片" />
|
||||
<ElButton type="danger" size="small" @click="handleRemoveImage" class="remove-btn">
|
||||
删除图片
|
||||
</ElButton>
|
||||
</div> -->
|
||||
|
||||
<ElUpload
|
||||
class="upload-demo"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:on-change="handleUploadChange"
|
||||
accept="image/*"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center py-5">
|
||||
<IconifyIcon icon="ep:upload-filled" class="mb-2.5 text-gray-400" style="font-size: 2.75rem !important;"/>
|
||||
<div class="ant-upload-text text-base text-gray-400">
|
||||
拖拽文件至此,或者
|
||||
<em class="cursor-pointer not-italic text-blue-500">
|
||||
选择文件
|
||||
</em>
|
||||
</div>
|
||||
<div class="mt-2.5 text-sm text-gray-400">
|
||||
已支持 pg/png 每个文件不超过
|
||||
1 MB。
|
||||
</div>
|
||||
</div>
|
||||
</ElUpload>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleDialogClose">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<ElDialog
|
||||
v-model="previewVisible"
|
||||
title="图片预览"
|
||||
width="800px"
|
||||
@close="handlePreviewClose"
|
||||
>
|
||||
<div class="preview-image-container">
|
||||
<ElImage
|
||||
:src="previewImageUrl"
|
||||
fit="contain"
|
||||
:preview-src-list="[previewImageUrl]"
|
||||
:initial-index="0"
|
||||
style="width: 100%; max-height: 600px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handlePreviewClose">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 更多价格对话框 -->
|
||||
<ElDialog
|
||||
v-model="morePriceDialogVisible"
|
||||
title="历史价格信息"
|
||||
width="700px"
|
||||
@close="handleMorePriceDialogClose"
|
||||
>
|
||||
<div class="more-price-container">
|
||||
<DbHst ref="moreHstRef" :settings="moreDbSettings" :contextMenuItems="moreContextMenuItems"></DbHst>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleMorePriceDialogClose">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElPopover
|
||||
:virtual-ref="popoverDateRangeRef"
|
||||
virtual-triggering
|
||||
:visible="visibleDateRange"
|
||||
:popper-style="{padding: '0px !important'}"
|
||||
:width="648"
|
||||
>
|
||||
<ElDatePickerPanel v-model="popoverDate" type="daterange" size="small" @calendar-change="handleCalendarChange" value-format="YYYY-MM-DD"/>
|
||||
</ElPopover>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
</style>
|
||||
128
apps/web-ele/src/views/database/entry/components/AddDialog.vue
Normal file
128
apps/web-ele/src/views/database/entry/components/AddDialog.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElDialog, ElForm, ElFormItem, ElInput, ElDatePicker, ElButton, ElMessage } from 'element-plus'
|
||||
|
||||
interface AddForm {
|
||||
name: string
|
||||
priceDate: string
|
||||
}
|
||||
|
||||
interface AttachmentData {
|
||||
id: number
|
||||
name: string
|
||||
priceDate: string
|
||||
attachment: string
|
||||
attachmentUrl: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
data: AttachmentData[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'update:data': [value: AttachmentData[]]
|
||||
}>()
|
||||
|
||||
const addFormRef = ref()
|
||||
const addForm = ref<AddForm>({
|
||||
name: '',
|
||||
priceDate: ''
|
||||
})
|
||||
|
||||
const addFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入信息价名称', trigger: 'blur' }
|
||||
],
|
||||
priceDate: [
|
||||
{ required: true, message: '请选择价格时间', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
const newId = props.data.length > 0
|
||||
? Math.max(...props.data.map(item => item.id)) + 1
|
||||
: 1
|
||||
|
||||
const newData = [...props.data, {
|
||||
id: newId,
|
||||
name: addForm.value.name,
|
||||
priceDate: addForm.value.priceDate,
|
||||
attachment: '',
|
||||
attachmentUrl: ''
|
||||
}]
|
||||
|
||||
emit('update:data', newData)
|
||||
ElMessage.success('新增成功')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error('请填写完整信息')
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
addFormRef.value?.resetFields()
|
||||
addForm.value = {
|
||||
name: '',
|
||||
priceDate: ''
|
||||
}
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="visible"
|
||||
title="新增信息价"
|
||||
width="500px"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@close="handleClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addFormRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="信息价名称" prop="name">
|
||||
<ElInput
|
||||
v-model="addForm.name"
|
||||
placeholder="请输入信息价名称"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="价格时间" prop="priceDate">
|
||||
<ElDatePicker
|
||||
v-model="addForm.priceDate"
|
||||
type="daterange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton @click="handleClose">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-form) {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElDialog, ElDatePicker, ElTable, ElTableColumn, ElButton, ElMessage } from 'element-plus'
|
||||
|
||||
interface AttachmentData {
|
||||
id: number
|
||||
name: string
|
||||
priceDate: string
|
||||
attachment: string
|
||||
attachmentUrl: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
data: AttachmentData[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'update:data': [value: AttachmentData[]]
|
||||
}>()
|
||||
|
||||
const attachmentDateRange = ref<[Date, Date] | null>(null)
|
||||
|
||||
const filteredAttachmentData = computed(() => {
|
||||
if (!attachmentDateRange.value || attachmentDateRange.value.length !== 2) {
|
||||
return props.data
|
||||
}
|
||||
|
||||
const [startDate, endDate] = attachmentDateRange.value
|
||||
return props.data.filter(item => {
|
||||
const itemDate = new Date(item.priceDate)
|
||||
return itemDate >= startDate && itemDate <= endDate
|
||||
})
|
||||
})
|
||||
|
||||
const handleUpload = (row: AttachmentData) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.pdf,.doc,.docx,.xls,.xlsx,.zip'
|
||||
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
const fileUrl = URL.createObjectURL(file)
|
||||
row.attachment = file.name
|
||||
row.attachmentUrl = fileUrl
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
}
|
||||
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleDownload = (row: AttachmentData) => {
|
||||
if (!row.attachmentUrl && !row.attachment) {
|
||||
ElMessage.warning('暂无附件可下载')
|
||||
return
|
||||
}
|
||||
|
||||
if (row.attachmentUrl) {
|
||||
const link = document.createElement('a')
|
||||
link.href = row.attachmentUrl
|
||||
link.download = row.attachment
|
||||
link.click()
|
||||
ElMessage.success('下载成功')
|
||||
} else {
|
||||
ElMessage.info('模拟下载: ' + row.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: AttachmentData) => {
|
||||
const index = props.data.findIndex(item => item.id === row.id)
|
||||
if (index > -1) {
|
||||
const newData = [...props.data]
|
||||
newData.splice(index, 1)
|
||||
emit('update:data', newData)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
attachmentDateRange.value = null
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="visible"
|
||||
title="附件管理"
|
||||
width="900px"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="attachment-container">
|
||||
<div class="mb-4 items-center gap-2">
|
||||
<span>价格时间段:</span>
|
||||
<ElDatePicker
|
||||
v-model="attachmentDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ElTable :data="filteredAttachmentData" border stripe style="width: 100%">
|
||||
<ElTableColumn prop="name" label="信息价名称" width="200" />
|
||||
<ElTableColumn prop="priceDate" label="价格时间段" width="150" />
|
||||
<ElTableColumn prop="attachment" label="附件" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.attachment">{{ row.attachment }}</span>
|
||||
<span v-else class="text-gray-400">暂无附件</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<ElButton type="primary" size="small" link @click="handleUpload(row)">
|
||||
上传
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleDownload(row)"
|
||||
:disabled="!row.attachment"
|
||||
link
|
||||
>
|
||||
下载
|
||||
</ElButton>
|
||||
<ElButton type="danger" size="small" link @click="handleDelete(row)">
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleClose">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.attachment-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDialog, ElTable, ElTableColumn, ElButton, ElMessage } from 'element-plus'
|
||||
|
||||
interface CopyData {
|
||||
id: number
|
||||
serialNumber: string
|
||||
tenantName: string
|
||||
specialty: string
|
||||
region: string
|
||||
priceDate: string
|
||||
priceName: string
|
||||
priceCount: number
|
||||
attachment: string
|
||||
published: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
data: CopyData[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'update:data': [value: CopyData[]]
|
||||
}>()
|
||||
|
||||
const handleTogglePublish = (row: CopyData) => {
|
||||
row.published = !row.published
|
||||
const status = row.published ? '已发布' : '未发布'
|
||||
ElMessage.success(`状态已更新为:${status}`)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="visible"
|
||||
title="软件商信息价管理"
|
||||
width="1200px"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="copy-container">
|
||||
<ElTable :data="data" border stripe style="width: 100%" max-height="600">
|
||||
<ElTableColumn prop="serialNumber" label="序号" width="80" fixed="left" />
|
||||
<ElTableColumn prop="tenantName" label="租户名称" width="120" />
|
||||
<ElTableColumn prop="specialty" label="专业" width="120" />
|
||||
<ElTableColumn prop="region" label="地区" width="120" />
|
||||
<ElTableColumn prop="priceDate" label="价格时间段" width="120" />
|
||||
<ElTableColumn prop="priceName" label="信息价名称" width="150" />
|
||||
<ElTableColumn prop="priceCount" label="信息价条数" width="120" align="center" />
|
||||
<ElTableColumn prop="attachment" label="附件" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.attachment">{{ row.attachment }}</span>
|
||||
<span v-else class="text-gray-400">暂无附件</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElButton
|
||||
:type="row.published ? 'success' : 'info'"
|
||||
size="small"
|
||||
@click="handleTogglePublish(row)"
|
||||
>
|
||||
{{ row.published ? '已发布' : '未发布' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleClose">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.copy-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElDialog, ElRow, ElCol, ElTreeV2, ElTable, ElTableColumn, ElButton, ElSplitter, ElSplitterPanel, ElImage } from 'element-plus'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const { height: containerHeight } = useElementSize(containerRef)
|
||||
interface HistoryTreeNode {
|
||||
id: string
|
||||
label: string
|
||||
children?: HistoryTreeNode[]
|
||||
}
|
||||
|
||||
interface HistoryTableData {
|
||||
id: number
|
||||
name: string
|
||||
model: string
|
||||
unit: string
|
||||
drawing: string
|
||||
category: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
// treeData: HistoryTreeNode[]
|
||||
// tableData: HistoryTableData[]
|
||||
}>()
|
||||
const treeData = ref<HistoryTreeNode[]>([
|
||||
{
|
||||
id: '1',
|
||||
label: '2024年',
|
||||
children: [
|
||||
{
|
||||
id: '1-1',
|
||||
label: '2024-01',
|
||||
children: [
|
||||
{ id: '1-1-1', label: '广东省信息价' },
|
||||
{ id: '1-1-2', label: '广西省信息价' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '1-2',
|
||||
label: '2024-02',
|
||||
children: [
|
||||
{ id: '1-2-1', label: '广东省信息价' },
|
||||
{ id: '1-2-2', label: '广西省信息价' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: '2023年',
|
||||
children: [
|
||||
{
|
||||
id: '2-1',
|
||||
label: '2023-12',
|
||||
children: [
|
||||
{ id: '2-1-1', label: '广东省信息价' },
|
||||
{ id: '2-1-2', label: '广西省信息价' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const tableData = ref<HistoryTableData[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '水泥',
|
||||
model: 'P.O 42.5',
|
||||
unit: '吨',
|
||||
drawing: 'https://via.placeholder.com/400x300',
|
||||
category: '材料'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '钢筋',
|
||||
model: 'HRB400E Φ12',
|
||||
unit: '吨',
|
||||
drawing: 'https://via.placeholder.com/400x300',
|
||||
category: '材料'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '混凝土',
|
||||
model: 'C30',
|
||||
unit: '立方米',
|
||||
drawing: '',
|
||||
category: '材料'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '砖',
|
||||
model: 'MU10',
|
||||
unit: '千块',
|
||||
drawing: 'https://via.placeholder.com/400x300',
|
||||
category: '材料'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '挖掘机',
|
||||
model: '1m³',
|
||||
unit: '台班',
|
||||
drawing: '',
|
||||
category: '机械'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '砖',
|
||||
model: 'MU10',
|
||||
unit: '千块',
|
||||
drawing: 'https://via.placeholder.com/400x300',
|
||||
category: '材料'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '挖掘机',
|
||||
model: '1m³',
|
||||
unit: '台班',
|
||||
drawing: '',
|
||||
category: '机械'
|
||||
}
|
||||
])
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'confirm': [node: any]
|
||||
}>()
|
||||
|
||||
const selectedHistoryNode = ref<any>(null)
|
||||
|
||||
const handleTreeSelect = (node: any) => {
|
||||
selectedHistoryNode.value = node
|
||||
console.log('选中的历史节点:', node)
|
||||
}
|
||||
const handleSelectionChange = (newSelection: any[]) => {
|
||||
console.log('handleSelectionChange:', newSelection)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
// emit('confirm', selectedHistoryNode.value)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
// selectedHistoryNode.value = null
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="visible"
|
||||
title="调用历史信息"
|
||||
width="50%"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="history-container" ref="containerRef">
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel size="20%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="treeData" @select="handleTreeSelect" :defaultExpandedKeys="2" :search="false" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||
<ElTable :data="tableData" border stripe style="width: 100%" height="380" @selection-change="handleSelectionChange">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn prop="name" label="名称" width="150" />
|
||||
<ElTableColumn prop="model" label="型号规格" width="150" />
|
||||
<ElTableColumn prop="unit" label="单位" width="100" align="center" />
|
||||
<ElTableColumn prop="drawing" label="图样(仅供参考)" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElImage
|
||||
v-if="row.drawing"
|
||||
:src="row.drawing"
|
||||
:preview-src-list="[row.drawing]"
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; cursor: pointer;"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else class="text-gray-400">暂无图样</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="category" label="分类" min-width="120" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton @click="handleClose">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-container {
|
||||
padding: 10px 0;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.history-tree-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.history-table-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
322
apps/web-ele/src/views/database/entry/index.ts
Normal file
322
apps/web-ele/src/views/database/entry/index.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { selectCellBorderStyle } from '#/components/db-hst/base'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
|
||||
// 操作列回调函数类型
|
||||
export interface OperationCallbacks {
|
||||
onUpload?: (row: number, data: any) => void
|
||||
onComplete?: (row: number, data: any) => void
|
||||
onPublish?: (row: number, data: any) => void
|
||||
onView?: (row: number, data: any) => void
|
||||
}
|
||||
|
||||
// 价格时间段渲染器回调类型
|
||||
export interface DateRangeCallbacks {
|
||||
onDateIconClick?: (row: number, data: any, event: MouseEvent) => void
|
||||
}
|
||||
|
||||
// 附件下载渲染器回调类型
|
||||
export interface AttachmentCallbacks {
|
||||
onDownload?: (row: number, data: any) => void
|
||||
}
|
||||
|
||||
// 价格时间段渲染器
|
||||
export function createDateRangeRenderer(callbacks: DateRangeCallbacks) {
|
||||
return function dateRangeRenderer(instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) {
|
||||
td.innerHTML = ''
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.justifyContent = 'space-between'
|
||||
container.style.gap = '8px'
|
||||
// container.style.padding = '0 8px'
|
||||
|
||||
// 文字部分
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value || ''
|
||||
textSpan.style.flex = '1'
|
||||
|
||||
// 时间图标
|
||||
const iconSpan = document.createElement('span')
|
||||
iconSpan.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
`
|
||||
iconSpan.style.cssText = `
|
||||
cursor: pointer;
|
||||
color: #bbbbbbff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
`
|
||||
|
||||
// iconSpan.addEventListener('mouseenter', () => {
|
||||
// iconSpan.style.color = '#66b1ff'
|
||||
// iconSpan.style.transform = 'scale(1.1)'
|
||||
// })
|
||||
|
||||
// iconSpan.addEventListener('mouseleave', () => {
|
||||
// iconSpan.style.color = '#409eff'
|
||||
// iconSpan.style.transform = 'scale(1)'
|
||||
// })
|
||||
|
||||
iconSpan.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
console.log('时间图标点击 - 行:', row, '数据:', rowData)
|
||||
|
||||
if (callbacks.onDateIconClick) {
|
||||
callbacks.onDateIconClick(row, rowData, e)
|
||||
}
|
||||
})
|
||||
|
||||
container.appendChild(textSpan)
|
||||
container.appendChild(iconSpan)
|
||||
td.appendChild(container)
|
||||
|
||||
return td
|
||||
}
|
||||
}
|
||||
|
||||
// 附件下载渲染器
|
||||
export function createAttachmentRenderer(callbacks: AttachmentCallbacks) {
|
||||
return function attachmentRenderer(instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) {
|
||||
td.innerHTML = ''
|
||||
|
||||
if (!value) {
|
||||
td.textContent = '-'
|
||||
td.style.textAlign = 'center'
|
||||
td.style.color = '#999'
|
||||
return td
|
||||
}
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.justifyContent = 'center'
|
||||
container.style.gap = '6px'
|
||||
|
||||
// 下载图标
|
||||
const iconSpan = document.createElement('span')
|
||||
iconSpan.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
`
|
||||
iconSpan.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #409eff;
|
||||
`
|
||||
|
||||
// 文件名
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = '下载'
|
||||
textSpan.style.cssText = `
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
text-decoration: underline;
|
||||
transition: all 0.3s;
|
||||
`
|
||||
|
||||
// 鼠标悬停效果
|
||||
const handleMouseEnter = () => {
|
||||
textSpan.style.color = '#66b1ff'
|
||||
iconSpan.style.color = '#66b1ff'
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
textSpan.style.color = '#409eff'
|
||||
iconSpan.style.color = '#409eff'
|
||||
}
|
||||
|
||||
container.addEventListener('mouseenter', handleMouseEnter)
|
||||
container.addEventListener('mouseleave', handleMouseLeave)
|
||||
|
||||
// 点击下载
|
||||
container.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
console.log('下载附件 - 行:', row, '数据:', rowData)
|
||||
|
||||
if (callbacks.onDownload) {
|
||||
callbacks.onDownload(row, rowData)
|
||||
}
|
||||
})
|
||||
|
||||
container.style.cursor = 'pointer'
|
||||
container.appendChild(iconSpan)
|
||||
container.appendChild(textSpan)
|
||||
td.appendChild(container)
|
||||
|
||||
return td
|
||||
}
|
||||
}
|
||||
|
||||
// 操作列渲染器
|
||||
export function createOperationRenderer(callbacks: OperationCallbacks) {
|
||||
return function operationRenderer(instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) {
|
||||
td.innerHTML = ''
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.gap = '4px'
|
||||
container.style.justifyContent = 'center'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.flexWrap = 'wrap'
|
||||
|
||||
const actions = [
|
||||
{ text: '附件上传', color: '#409eff', callback: callbacks.onUpload },
|
||||
{ text: '完成', color: '#409eff', callback: callbacks.onComplete },
|
||||
{ text: '发布', color: '#409eff', callback: callbacks.onPublish },
|
||||
{ text: '查看', color: '#409eff', callback: callbacks.onView }
|
||||
]
|
||||
|
||||
actions.forEach(action => {
|
||||
const btn = document.createElement('span')
|
||||
btn.textContent = action.text
|
||||
btn.style.cssText = `
|
||||
cursor: pointer;
|
||||
color: ${action.color};
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
// btn.addEventListener('mouseenter', () => {
|
||||
// btn.style.backgroundColor = action.color
|
||||
// btn.style.color = '#fff'
|
||||
// })
|
||||
|
||||
// btn.addEventListener('mouseleave', () => {
|
||||
// btn.style.backgroundColor = 'transparent'
|
||||
// btn.style.color = action.color
|
||||
// })
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
console.log(`${action.text} 操作 - 行:`, row, '数据:', rowData)
|
||||
|
||||
// 调用回调函数
|
||||
if (action.callback) {
|
||||
action.callback(row, rowData)
|
||||
}
|
||||
})
|
||||
|
||||
container.appendChild(btn)
|
||||
})
|
||||
|
||||
td.appendChild(container)
|
||||
return td
|
||||
}
|
||||
}
|
||||
|
||||
export const contextMenuHandler = {
|
||||
rootKey: 'add-category',
|
||||
rootText: '添加总类',
|
||||
onRootAdd: async (newNode: any, allRootNodes: any[]) => {
|
||||
console.log('添加总类:', newNode, '所有根节点:', allRootNodes)
|
||||
|
||||
// 计算所有根节点的最大 sortOrder
|
||||
const maxSortOrder = allRootNodes.reduce((max, node) => {
|
||||
const sortOrder = node.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
const newSortOrder = maxSortOrder + 1
|
||||
newNode.code = 'ROOT'
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('新节点 sortOrder:', newSortOrder)
|
||||
},
|
||||
onRootDelete: async (node: any) => {
|
||||
console.log('删除总类:', node)
|
||||
// TODO: 调用 API 删除根节点
|
||||
// await deleteCategories(node.id)
|
||||
},
|
||||
levels: [
|
||||
{
|
||||
depth: 0,
|
||||
addKey: 'add-province',
|
||||
addText: '添加省市',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加省市 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 计算当前父节点下所有子节点的最大 sortOrder
|
||||
const maxSortOrder = allChildren.reduce((max, child) => {
|
||||
const sortOrder = child.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
console.log('父节点下子节点最大 sortOrder:', maxSortOrder)
|
||||
|
||||
// TODO: 调用 API 保存
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除省市:', node)
|
||||
// TODO: 调用 API 删除
|
||||
// await deleteCatalog(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
addKey: 'add-specialty',
|
||||
addText: '添加工料机专业',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加工料机专业 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 计算当前父节点下所有子节点的最大 sortOrder
|
||||
const maxSortOrder = allChildren.reduce((max, child) => {
|
||||
const sortOrder = child.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
console.log('父节点下子节点最大 sortOrder:', maxSortOrder)
|
||||
|
||||
// TODO: 调用 API 保存
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除工料机专业:', node)
|
||||
// TODO: 调用 API 删除
|
||||
// await deleteCatalog(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
allowDelete: true,
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除第三层节点:', node)
|
||||
// TODO: 调用 API 删除第三层节点
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ const categoryTreeData = ref<Tree[]>([
|
||||
}
|
||||
])
|
||||
const topColumns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码', renderer: codeRenderer},
|
||||
{type:'text',data:'code',title:'编码', renderer: codeRenderer, code:true},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位'},
|
||||
])
|
||||
|
||||
@@ -233,12 +233,12 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel collapsible size="15%" :min="200">
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="categoryTreeData" @select="categoryHandleSelect" :defaultExpandedKeys="2" :search="false" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
||||
</ElCard>
|
||||
|
||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
||||
const hstRef = ref<any>(null)
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer },
|
||||
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer, code:true },
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'feature', title: '项目特征' },
|
||||
|
||||
@@ -77,7 +77,7 @@ const dialogRenderer = (instance: any, td: HTMLElement, row: number, col: number
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', renderer: codeRenderer },
|
||||
{ type: 'text', data: 'name', title: '费用名称', renderer: codeRenderer , code:true},
|
||||
{ type: 'text', data: 'spec', title: '费用代码', width: 300,renderer: dialogRenderer },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数(用户端不显示)', width: 200 },
|
||||
])
|
||||
|
||||
@@ -39,28 +39,107 @@ export const treeData = ref<Tree[]>([
|
||||
export const contextMenuHandler = {
|
||||
rootKey: 'add-category',
|
||||
rootText: '添加定额',
|
||||
onRootAdd: async (newNode: any, allRootNodes: any[]) => {
|
||||
console.log('添加总类:', newNode, '所有根节点:', allRootNodes)
|
||||
|
||||
// 计算所有根节点的最大 sortOrder
|
||||
const maxSortOrder = allRootNodes.reduce((max, node) => {
|
||||
const sortOrder = node.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
const newSortOrder = maxSortOrder + 1
|
||||
newNode.code = 'ROOT'
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('新节点 sortOrder:', newSortOrder)
|
||||
},
|
||||
onRootDelete: async (node: any) => {
|
||||
console.log('删除总类:', node)
|
||||
// TODO: 调用 API 删除根节点
|
||||
// await deleteCategories(node.id)
|
||||
},
|
||||
levels: [
|
||||
{
|
||||
depth: 0,
|
||||
addKey: 'add-province',
|
||||
addText: '添加省市',
|
||||
allowDelete: true
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加省市 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 计算当前父节点下所有子节点的最大 sortOrder
|
||||
const maxSortOrder = allChildren.reduce((max, child) => {
|
||||
const sortOrder = child.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
console.log('父节点下子节点最大 sortOrder:', maxSortOrder)
|
||||
|
||||
// TODO: 调用 API 保存省市节点
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除省市:', node)
|
||||
// TODO: 调用 API 删除节点
|
||||
// await deleteCatalog(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
addKey: 'add-name',
|
||||
addText: '添加名称',
|
||||
allowDelete: true
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加名称 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 计算当前父节点下所有子节点的最大 sortOrder
|
||||
const maxSortOrder = allChildren.reduce((max, child) => {
|
||||
const sortOrder = child.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
console.log('父节点下子节点最大 sortOrder:', maxSortOrder)
|
||||
|
||||
// TODO: 调用 API 保存省市节点
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除名称:', node)
|
||||
// TODO: 调用 API 删除省市节点
|
||||
// await deleteCatalog(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
addKey: 'add-model',
|
||||
addText: '添加模式',
|
||||
allowDelete: true
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加模式 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 计算当前父节点下所有子节点的最大 sortOrder
|
||||
const maxSortOrder = allChildren.reduce((max, child) => {
|
||||
const sortOrder = child.sortOrder ?? 0
|
||||
return Math.max(max, sortOrder)
|
||||
}, 0)
|
||||
|
||||
console.log('父节点下子节点最大 sortOrder:', maxSortOrder)
|
||||
|
||||
// TODO: 调用 API 创建
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除:', node)
|
||||
// TODO: 调用 API 删除
|
||||
// await deleteCatalog(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
allowDelete: true
|
||||
allowDelete: true,
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除第三层节点:', node)
|
||||
// TODO: 调用 API 删除第三层节点
|
||||
// await deleteCategoriesTree(node.id)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed } from 'vue'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElSplitter,ElSplitterPanel,ElCard,ElDialog,ElTable,ElTableColumn,ElButton,ElInput,ElAlert } from 'element-plus';
|
||||
import { ElSplitter,ElSplitterPanel,ElCard,ElDialog,ElTable,ElTableColumn,ElButton,ElInput,ElAlert,ElTreeV2 } from 'element-plus';
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
|
||||
import { treeData as catalogTreeData, contextMenuHandler as catalogContextMenuHandler } from './catalog';
|
||||
import { treeData as topTreeData, contextMenuHandler as topContextMenuHandler } from './catalog';
|
||||
import { dbSettings as projectDbSettings } from './project';
|
||||
import { dbSettings as subDbSettings } from './sub';
|
||||
import { dbSettings as guideDbSettings } from './guide';
|
||||
import { dbSettings as bottomDbSettings } from './rightHst';
|
||||
import { dbSettings as projectDbSettings, contextMenuItems as projectContextMenuItems, initProjectHst, load as projectLoad } from './project';
|
||||
import { dbSettings as subDbSettings, contextMenuItems as subContextMenuItems, setEditDescCallback, initSubHst, load as subLoad } from './sub';
|
||||
import { dbSettings as guideDbSettings, contextMenuItems as guideContextMenuItems, initGuideHst, load as guideLoad } from './guide';
|
||||
import { dbSettings as bottomDbSettings, contextMenuItems as bottomContextMenuItems, initBottomHst, load as bottomLoad } from './rightHst';
|
||||
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
@@ -29,66 +30,221 @@ const subHstRef = ref<any>(null)
|
||||
const guideHstRef = ref<any>(null)
|
||||
const bottomHstRef = ref<any>(null)
|
||||
|
||||
// Dialog state
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTreeData = ref<any[]>([])
|
||||
const selectedNode = ref<any>(null)
|
||||
|
||||
// Rich text editor dialog state
|
||||
const richTextDialogVisible = ref(false)
|
||||
const richTextContent = ref('')
|
||||
const currentEditRow = ref<number>(-1)
|
||||
const currentEditCol = ref<number>(-1)
|
||||
|
||||
// 生成100条模拟数据
|
||||
function generateMockData() {
|
||||
const units = ['项', 'm', 'm²', 'm³', 't', 'kg', '个', '套', '台', '组']
|
||||
const categories = ['土建工程', '装饰工程', '安装工程', '市政工程', '园林工程', '钢结构工程', '幕墙工程', '消防工程', '电气工程', '给排水工程']
|
||||
const subCategories = ['基础工程', '主体结构', '屋面工程', '门窗工程', '防水工程', '保温工程', '抹灰工程', '涂饰工程', '吊顶工程', '地面工程']
|
||||
|
||||
const data = []
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const categoryIndex = (i - 1) % categories.length
|
||||
const hasChildren = i <= 50 // 前50条有子项
|
||||
|
||||
const item: any = {
|
||||
code: `PRJ${String(i).padStart(3, '0')}`,
|
||||
name: `${categories[categoryIndex]}-${i}`,
|
||||
unit: units[i % units.length],
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 为前50条数据添加2-5个子项
|
||||
if (hasChildren) {
|
||||
const childCount = Math.floor(Math.random() * 4) + 2 // 2-5个子项
|
||||
for (let j = 1; j <= childCount; j++) {
|
||||
const subItem: any = {
|
||||
code: `${item.code}-${String(j).padStart(3, '0')}`,
|
||||
name: `${subCategories[j % subCategories.length]}-${i}-${j}`,
|
||||
unit: units[(i + j) % units.length],
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 部分子项再添加孙项
|
||||
if (i <= 20 && j <= 2) {
|
||||
const grandChildCount = Math.floor(Math.random() * 3) + 1 // 1-3个孙项
|
||||
for (let k = 1; k <= grandChildCount; k++) {
|
||||
subItem.__children.push({
|
||||
code: `${subItem.code}-${String(k).padStart(3, '0')}`,
|
||||
name: `细项-${i}-${j}-${k}`,
|
||||
unit: units[(i + j + k) % units.length],
|
||||
__children: []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
item.__children.push(subItem)
|
||||
}
|
||||
}
|
||||
|
||||
data.push(item)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
const treeHandleSelect = (node: any) => {
|
||||
console.log('treeHandleSelect',node)
|
||||
console.log('treeHandleSelect', node)
|
||||
|
||||
// 使用 projectLoad 加载数据到 projectHstRef
|
||||
if (node && projectHstRef.value?.hotInstance) {
|
||||
// 根据树节点加载对应的数据
|
||||
// 示例1: 如果 node 包含数据数组
|
||||
// projectLoad(node.data)
|
||||
|
||||
// 示例2: 如果需要根据 node.id 从 API 获取数据
|
||||
// fetchProjectData(node.id).then(data => {
|
||||
// projectLoad(data)
|
||||
// })
|
||||
|
||||
// 示例3: 加载模拟数据(用于测试)
|
||||
const mockData = generateMockData()
|
||||
projectLoad(mockData)
|
||||
}
|
||||
}
|
||||
function onResizeEnd(index: number, sizes: number[]) {
|
||||
// 通过 hotInstance 来操作 Handsontable
|
||||
console.log(index,sizes)
|
||||
// console.log(index,sizes)
|
||||
onTopHeight(sizes[0])
|
||||
onBottomHeight(sizes[1])
|
||||
}
|
||||
function onTopHeight(height: number){
|
||||
// if (topHstRef.value?.hotInstance) {
|
||||
// topHstRef.value.hotInstance.updateSettings({
|
||||
// height: height-15,
|
||||
// })
|
||||
|
||||
// topHstRef.value.hotInstance.render()
|
||||
|
||||
// // console.log('onResizeEnd-onTopHeight',height, 'codeColWidth:', topHstRef.value.codeColWidth);
|
||||
// }
|
||||
topContainerHeight.value = height
|
||||
}
|
||||
function onBottomHeight(height: number){
|
||||
// if (bottomHstRef.value?.hotInstance) {
|
||||
// bottomHstRef.value.hotInstance.updateSettings({
|
||||
// height: height-15
|
||||
// })
|
||||
|
||||
// // bottomHstRef.value.loadData(bootomMock())
|
||||
// // bottomHstRef.value.hotInstance.render()
|
||||
// console.log('onResizeEnd-bottomHstRef',height);
|
||||
// }
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
bottomHstRef.value.hotInstance.updateSettings({
|
||||
height: height
|
||||
})
|
||||
bottomHstRef.value.hotInstance.render()
|
||||
}
|
||||
}
|
||||
function onDecide(node: any){
|
||||
console.log('onDecide',node)
|
||||
selectedNode.value = node
|
||||
// 设置弹窗树数据(这里使用示例数据,你可以根据需要修改)
|
||||
dialogTreeData.value = [
|
||||
{
|
||||
id: '1',
|
||||
label: '选项 1',
|
||||
children: [
|
||||
{ id: '1-1', label: '选项 1-1' },
|
||||
{ id: '1-2', label: '选项 1-2' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: '选项 2',
|
||||
children: [
|
||||
{ id: '2-1', label: '选项 2-1' },
|
||||
{ id: '2-2', label: '选项 2-2' }
|
||||
]
|
||||
}
|
||||
]
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleDialogTreeClick(data: any) {
|
||||
console.log('选中的树节点:', data)
|
||||
}
|
||||
|
||||
function handleDialogConfirm() {
|
||||
selectedNode.value.selected = true
|
||||
console.log('确认选择:', selectedNode.value)
|
||||
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
function handleDialogCancel() {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 处理富文本编辑
|
||||
function handleEditDesc(row: number, col: number, value: string) {
|
||||
currentEditRow.value = row
|
||||
currentEditCol.value = col
|
||||
richTextContent.value = value
|
||||
richTextDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 确认富文本编辑
|
||||
function handleRichTextConfirm() {
|
||||
if (subHstRef.value?.hotInstance && currentEditRow.value >= 0) {
|
||||
subHstRef.value.hotInstance.setDataAtCell(currentEditRow.value, currentEditCol.value, richTextContent.value)
|
||||
}
|
||||
richTextDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 取消富文本编辑
|
||||
function handleRichTextCancel() {
|
||||
richTextDialogVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// 设置编辑回调
|
||||
setEditDescCallback(handleEditDesc)
|
||||
setTimeout(() => {
|
||||
// onTopHeight(topContainerHeight.value)
|
||||
// onBottomHeight(bottomContainerHeight.value)
|
||||
|
||||
if (projectHstRef.value?.hotInstance) {
|
||||
// 初始化 projectHstRef
|
||||
initProjectHst(projectHstRef, (row: number | null) => {
|
||||
console.log('projectHstRef-选中的行:', row)
|
||||
// 在这里处理点击事件
|
||||
subLoad(generateMockData())
|
||||
})
|
||||
projectHstRef.value?.hotInstance.updateSettings({
|
||||
height: containerHeight.value - 15
|
||||
height: containerHeight.value
|
||||
})
|
||||
// 更新 code 列的宽度
|
||||
projectHstRef.value.updateCodeColWidth()
|
||||
// projectHstRef.value.updateCodeColWidth()
|
||||
}
|
||||
if (subHstRef.value?.hotInstance) {
|
||||
// 初始化 subHstRef
|
||||
initSubHst(subHstRef, (row: number | null) => {
|
||||
console.log('subHstRef-选中的行:', row)
|
||||
// 在这里处理点击事件
|
||||
guideLoad(generateMockData())
|
||||
})
|
||||
subHstRef.value?.hotInstance.updateSettings({
|
||||
height: containerHeight.value - 15
|
||||
height: containerHeight.value
|
||||
})
|
||||
|
||||
}
|
||||
if (guideHstRef.value?.hotInstance) {
|
||||
// 初始化 guideHstRef
|
||||
initGuideHst(guideHstRef, (row: number | null) => {
|
||||
console.log('guideHstRef-选中的行:', row)
|
||||
// 在这里处理点击事件
|
||||
bottomLoad(generateMockData())
|
||||
})
|
||||
guideHstRef.value?.hotInstance.updateSettings({
|
||||
height: containerHeight.value - 15
|
||||
height: containerHeight.value
|
||||
})
|
||||
}
|
||||
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
// 初始化 bottomHstRef
|
||||
initBottomHst(bottomHstRef, (row: number | null) => {
|
||||
console.log('bottomHstRef-选中的行:', row)
|
||||
// 在这里处理点击事件
|
||||
})
|
||||
bottomHstRef.value?.hotInstance.updateSettings({
|
||||
height: containerHeight.value
|
||||
height: bottomContainerHeight.value
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,41 +258,85 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel collapsible size="15%" :min="200">
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="catalogTreeData" @select="treeHandleSelect" :defaultExpandedKeys="3" :context-menu-handler="catalogContextMenuHandler" :search="false" />
|
||||
<DbTree :height="containerHeight" :data="catalogTreeData" @select="treeHandleSelect"
|
||||
:defaultExpandedKeys="3" :context-menu-handler="catalogContextMenuHandler" :search="false"
|
||||
:decide="true" @decide="onDecide"
|
||||
:contextmenu="true"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" >
|
||||
<DbHst ref="projectHstRef" :settings="projectDbSettings"></DbHst>
|
||||
<DbHst ref="projectHstRef" :settings="projectDbSettings" :contextMenuItems="projectContextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" >
|
||||
<DbHst ref="subHstRef" :settings="subDbSettings"></DbHst>
|
||||
<DbHst ref="subHstRef" :settings="subDbSettings" :contextMenuItems="subContextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible size="18%" :min="200">
|
||||
<ElSplitterPanel size="18%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" >
|
||||
<DbHst ref="guideHstRef" :settings="guideDbSettings"></DbHst>
|
||||
<DbHst ref="guideHstRef" :settings="guideDbSettings" :contextMenuItems="guideContextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitterPanel :min="200">
|
||||
<ElSplitterPanel size="20%">
|
||||
<ElSplitter layout="vertical" @resize-end="onResizeEnd">
|
||||
<ElSplitterPanel collapsible size="50%" :min="200">
|
||||
<ElSplitterPanel size="50%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="topContainerRef">
|
||||
<DbTree :height="topContainerHeight" :data="topTreeData" @select="treeHandleSelect" :defaultExpandedKeys="3" :context-menu-handler="topContextMenuHandler" :search="false" />
|
||||
<!-- :context-menu-handler="topContextMenuHandler" -->
|
||||
<DbTree :height="topContainerHeight" :data="topTreeData" @select="treeHandleSelect"
|
||||
:defaultExpandedKeys="3" :search="false" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="bottomContainerRef">
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings" :contextMenuItems="bottomContextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
|
||||
<!-- 树选择弹窗 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="选择节点"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElTreeV2
|
||||
:data="dialogTreeData"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' }"
|
||||
:height="400"
|
||||
highlight-current
|
||||
@node-click="handleDialogTreeClick"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleDialogCancel">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleDialogConfirm">确定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 富文本编辑弹窗 -->
|
||||
<ElDialog
|
||||
v-model="richTextDialogVisible"
|
||||
title="编辑清单说明"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<RichTextarea v-model="richTextContent" height="400px" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleRichTextCancel">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleRichTextConfirm">确定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,33 +1,108 @@
|
||||
//清单指引
|
||||
import { ref } from 'vue'
|
||||
const columns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位',width:30},
|
||||
import { ref, nextTick } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
|
||||
])
|
||||
// 用于存储 guideHstRef 的引用
|
||||
let guideHstRef: Ref<any> | null = null
|
||||
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||
|
||||
const mockData = ()=>{
|
||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
||||
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `A${String(index + 1).padStart(6, '0')}`,
|
||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
||||
unit: units[index % units.length],
|
||||
}))
|
||||
return mockData;
|
||||
// 回调函数,供 config.vue 使用
|
||||
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||
|
||||
// 初始化函数,由 config.vue 调用
|
||||
export function initGuideHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||
guideHstRef = hstRef
|
||||
onCellMouseDownCallback = callback || null
|
||||
}
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
|
||||
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => guideHstRef?.value?.hotInstance,
|
||||
})
|
||||
export let dbSettings = {
|
||||
data: mockData(),
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 100,
|
||||
columns: columns.value
|
||||
// 包装 load 函数,在加载前重置选中行
|
||||
export function load(data: any[]) {
|
||||
selectedRow.value = null
|
||||
return originalLoad(data)
|
||||
}
|
||||
const columns:any[] = [
|
||||
{type:'text',data:'number',title:'序号', width: 50},
|
||||
{type:'text',data:'code',title:'编码',renderer: codeRenderer, code:true},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位',width:50, className: 'htCenter'},
|
||||
|
||||
]
|
||||
|
||||
export let dbSettings = {
|
||||
data: [],
|
||||
dataSchema: initSchema(columns),
|
||||
colWidths: 160,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
separator1: '---------',
|
||||
custom_add_child: {
|
||||
name: '添加子行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'child')
|
||||
}
|
||||
},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'delete')
|
||||
}
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
// 只能单击
|
||||
if (event.button !== 0) {
|
||||
return // 直接返回,不执行后续逻辑
|
||||
}
|
||||
selectedRow.value = coords
|
||||
|
||||
// 触发回调函数
|
||||
if (onCellMouseDownCallback) {
|
||||
onCellMouseDownCallback(selectedRow.value)
|
||||
}
|
||||
},
|
||||
afterDeselect(){
|
||||
highlightDeselect(this, selectedRow.value)
|
||||
}
|
||||
}
|
||||
// 定义 topHstRef 的自定义右键菜单
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
handleRowOperation(hotInstance, 'append')
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -1,75 +1,47 @@
|
||||
//工程
|
||||
import { ref } from 'vue'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
const columns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编号',renderer: codeRenderer},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位', width: 50},
|
||||
])
|
||||
const mockData = ()=>{
|
||||
const mockData = []
|
||||
let codeCounter = 1
|
||||
|
||||
// 生成5个父级工程项目
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const parentCode = `PRJ${String(codeCounter++).padStart(2, '0')}`
|
||||
const parent = {
|
||||
code: parentCode,
|
||||
name: `工程分类${i + 1}`,
|
||||
unit: ['项', 'm³', 'm²', 't', '套'][i % 5],
|
||||
level: String(i),
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 为每个父级生成2-4个子项
|
||||
const childCount = Math.floor(Math.random() * 3) + 2
|
||||
for (let j = 0; j < childCount; j++) {
|
||||
const childCode = `${parentCode}-${String(j + 1).padStart(3, '0')}`
|
||||
const child = {
|
||||
code: childCode,
|
||||
name: `${parent.name}-子项${j + 1}`,
|
||||
unit: ['m³', 'm²', 't', '套', '台'][j % 5],
|
||||
level: `${i}-${j + 1}`,
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 为部分子项生成孙项
|
||||
if (j === 0 && i < 3) {
|
||||
const grandChildCount = Math.floor(Math.random() * 2) + 1
|
||||
for (let k = 0; k < grandChildCount; k++) {
|
||||
const grandChild = {
|
||||
code: `${childCode}-${String(k + 1).padStart(2, '0')}`,
|
||||
name: `${child.name}-明细${k + 1}`,
|
||||
unit: ['m³', 'm²', 't', 'kg', '个'][k % 5],
|
||||
level: `${i}-${j + 1}-${k + 1}`,
|
||||
__children: []
|
||||
}
|
||||
child.__children.push(grandChild)
|
||||
}
|
||||
}
|
||||
|
||||
parent.__children.push(child)
|
||||
}
|
||||
|
||||
mockData.push(parent)
|
||||
}
|
||||
|
||||
return mockData
|
||||
import { ref, nextTick } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
|
||||
// 用于存储 projectHstRef 的引用
|
||||
let projectHstRef: Ref<any> | null = null
|
||||
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||
|
||||
// 回调函数,供 config.vue 使用
|
||||
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||
|
||||
// 初始化函数,由 config.vue 调用
|
||||
export function initProjectHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||
projectHstRef = hstRef
|
||||
onCellMouseDownCallback = callback || null
|
||||
}
|
||||
let rowSchema: any = {level: null, __children: []}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data && col.data !== 'level' && col.data !== '__children') {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
|
||||
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => projectHstRef?.value?.hotInstance,
|
||||
})
|
||||
|
||||
// 包装 load 函数,在加载前重置选中行
|
||||
export function load(data: any[]) {
|
||||
selectedRow.value = null
|
||||
return originalLoad(data)
|
||||
}
|
||||
const columns:any[] = [
|
||||
{type:'text',data:'number',title:'序号', width: 50, className: 'htCenter'},
|
||||
{type:'text',data:'code',title:'编号',renderer: codeRenderer, code:true},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位', width: 60, className: 'htCenter'},
|
||||
{type:'text',data:'text1',title:'编码增加数', width: 100},
|
||||
{type:'text',data:'text2',title:'尾节点', width: 100},
|
||||
{type:'text',data:'text3',title:'子目编码增加数', width: 110},
|
||||
]
|
||||
|
||||
export let dbSettings = {
|
||||
data: mockData(),
|
||||
dataSchema: rowSchema,
|
||||
data: [],
|
||||
dataSchema: initSchema(columns),
|
||||
colWidths: 160,
|
||||
columns: columns.value,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: true,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
@@ -104,5 +76,37 @@ export let dbSettings = {
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
// 只能单击
|
||||
if (event.button !== 0) {
|
||||
return // 直接返回,不执行后续逻辑
|
||||
}
|
||||
selectedRow.value = coords
|
||||
|
||||
// 触发回调函数
|
||||
if (onCellMouseDownCallback) {
|
||||
onCellMouseDownCallback(selectedRow.value)
|
||||
}
|
||||
},
|
||||
afterDeselect(){
|
||||
highlightDeselect(this, selectedRow.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 定义 topHstRef 的自定义右键菜单
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
handleRowOperation(hotInstance, 'append')
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -1,33 +1,108 @@
|
||||
//右下角
|
||||
import { ref } from 'vue'
|
||||
const columns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位',width:30},
|
||||
import { ref, nextTick } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
|
||||
])
|
||||
// 用于存储 bottomHstRef 的引用
|
||||
let bottomHstRef: Ref<any> | null = null
|
||||
const selectedRow = ref<number | null>(null) // 记录当前选中的行
|
||||
|
||||
const mockData = ()=>{
|
||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
||||
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `B${String(index + 1).padStart(6, '0')}`,
|
||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
||||
unit: units[index % units.length],
|
||||
}))
|
||||
return mockData;
|
||||
// 回调函数,供 config.vue 使用
|
||||
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||
|
||||
// 初始化函数,由 config.vue 调用
|
||||
export function initBottomHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||
bottomHstRef = hstRef
|
||||
onCellMouseDownCallback = callback || null
|
||||
}
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
|
||||
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => bottomHstRef?.value?.hotInstance,
|
||||
})
|
||||
export let dbSettings = {
|
||||
data: mockData(),
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 100,
|
||||
columns: columns.value
|
||||
// 包装 load 函数,在加载前重置选中行
|
||||
export function load(data: any[]) {
|
||||
selectedRow.value = null
|
||||
return originalLoad(data)
|
||||
}
|
||||
const columns :any[] = [
|
||||
{type:'text',data:'number',title:'序号', width: 50},
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称', className: 'htCenter'},
|
||||
{type:'text',data:'unit',title:'单位',width:50},
|
||||
|
||||
]
|
||||
|
||||
export let dbSettings = {
|
||||
data: [],
|
||||
dataSchema: initSchema(columns),
|
||||
colWidths: 100,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
separator1: '---------',
|
||||
custom_add_child: {
|
||||
name: '添加子行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'child')
|
||||
}
|
||||
},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'delete')
|
||||
}
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
// 只能单击
|
||||
if (event.button !== 0) {
|
||||
return // 直接返回,不执行后续逻辑
|
||||
}
|
||||
selectedRow.value = coords
|
||||
|
||||
// 触发回调函数
|
||||
if (onCellMouseDownCallback) {
|
||||
onCellMouseDownCallback(selectedRow.value)
|
||||
}
|
||||
},
|
||||
afterDeselect(){
|
||||
highlightDeselect(this, selectedRow.value)
|
||||
}
|
||||
}
|
||||
// 定义 topHstRef 的自定义右键菜单
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
handleRowOperation(hotInstance, 'below')
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,35 +1,144 @@
|
||||
//清单子目
|
||||
import { ref } from 'vue'
|
||||
const columns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位',width:30},
|
||||
{type:'text',data:'desc',title:'清单说明'},
|
||||
import { ref, nextTick } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
import { selectCellBorderStyle } from '#/components/db-hst/base'
|
||||
|
||||
])
|
||||
// 用于存储 subHstRef 的引用
|
||||
let subHstRef: Ref<any> | null = null
|
||||
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||
|
||||
const mockData = ()=>{
|
||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
||||
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `MAT${String(index + 1).padStart(6, '0')}`,
|
||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
||||
unit: units[index % units.length],
|
||||
desc: `这是详细说明,`
|
||||
}))
|
||||
return mockData;
|
||||
// 回调函数,供 config.vue 使用
|
||||
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||
|
||||
// 初始化函数,由 config.vue 调用
|
||||
export function initSubHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||
subHstRef = hstRef
|
||||
onCellMouseDownCallback = callback || null
|
||||
}
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
|
||||
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => subHstRef?.value?.hotInstance,
|
||||
})
|
||||
export let dbSettings = {
|
||||
data: mockData(),
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 100,
|
||||
columns: columns.value
|
||||
|
||||
// 包装 load 函数,在加载前重置选中行
|
||||
export function load(data: any[]) {
|
||||
selectedRow.value = null
|
||||
return originalLoad(data)
|
||||
}
|
||||
// 用于存储编辑回调函数
|
||||
let editDescCallback: ((row: number, col: number, value: string) => void) | null = null
|
||||
|
||||
// 设置编辑回调
|
||||
export function setEditDescCallback(callback: (row: number, col: number, value: string) => void) {
|
||||
editDescCallback = callback
|
||||
}
|
||||
|
||||
// 自定义渲染器
|
||||
function editRenderer(instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) {
|
||||
// 清空单元格内容
|
||||
td.innerHTML = ''
|
||||
|
||||
// 创建链接元素
|
||||
const link = document.createElement('a')
|
||||
link.href = 'javascript:void(0)'
|
||||
link.textContent = '说明'
|
||||
// link.textContent = value || '点击编辑'
|
||||
link.style.color = '#409eff'
|
||||
link.style.textDecoration = 'underline'
|
||||
link.style.cursor = 'pointer'
|
||||
|
||||
// 点击事件
|
||||
link.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (editDescCallback) {
|
||||
editDescCallback(row, col, value || '')
|
||||
}
|
||||
})
|
||||
|
||||
td.appendChild(link)
|
||||
return td
|
||||
}
|
||||
const columns:any[] = [
|
||||
{type:'text',data:'number',title:'序号', width: 50},
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'unit',title:'单位',width:50, className: 'htCenter'},
|
||||
{type:'text',data:'desc',title:'清单说明',renderer: editRenderer},
|
||||
]
|
||||
|
||||
export let dbSettings = {
|
||||
data: [],
|
||||
dataSchema: initSchema(columns),
|
||||
colWidths: 100,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
separator1: '---------',
|
||||
custom_add_child: {
|
||||
name: '添加子行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'child')
|
||||
}
|
||||
},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'delete')
|
||||
}
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
// 只能单击
|
||||
if (event.button !== 0) {
|
||||
return // 直接返回,不执行后续逻辑
|
||||
}
|
||||
selectedRow.value = coords
|
||||
|
||||
// 触发回调函数
|
||||
if (onCellMouseDownCallback) {
|
||||
onCellMouseDownCallback(selectedRow.value)
|
||||
}
|
||||
},
|
||||
afterDeselect(){
|
||||
highlightDeselect(this, selectedRow.value)
|
||||
}
|
||||
}
|
||||
// 定义 topHstRef 的自定义右键菜单
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
handleRowOperation(hotInstance, 'below')
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,22 +1,426 @@
|
||||
export const materialsColHeaders: string[] = [
|
||||
'名称',
|
||||
'类别',
|
||||
'除税基价代码',
|
||||
'含税基价代码',
|
||||
'除税编制代码',
|
||||
'含税编制代码',
|
||||
import { createCatalogItem, createCategoriesTree, deleteCatalogItem, deleteCategoriesTree } from '#/api/database/materials/index';
|
||||
import { deleteCategories } from '#/api/database/materials/root';
|
||||
import { ElMessage } from 'element-plus';
|
||||
type Tree = { id: string; label: string; children?: Tree[]; [key: string]: any }
|
||||
|
||||
// 生成随机6位数字+英文字符串
|
||||
const generateRandomCode = (): string => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
// 递归转换树结构:name -> label,保留所有原始属性
|
||||
export const transformTreeData = (data: any[]): Tree[] => {
|
||||
return data.map(item => ({
|
||||
...item, // 保留所有原始属性
|
||||
id: String(item.id), // 确保ID是字符串
|
||||
label: item.name,
|
||||
children: item.children && item.children.length > 0
|
||||
? transformTreeData(item.children)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
// 自定义 renderer:检测重复的 code 值并标红
|
||||
/*const duplicateCodeRenderer = (instance: any, td: HTMLElement, _row: number, _col: number, _prop: string, value: any, _cellProperties: any) => {
|
||||
// 检查是否需要设置验证背景色
|
||||
// const columns = instance.getSettings().columns || []
|
||||
// const currentColumn = columns[_col]
|
||||
// const isEmpty = value === null || value === undefined || String(value).trim() === ''
|
||||
|
||||
// // 如果列配置了 allowInvalid: true 且值为空,设置红色背景
|
||||
// if (currentColumn?.allowInvalid === true && isEmpty) {
|
||||
|
||||
// } else {
|
||||
// td.style.backgroundColor = '' // 清除背景色
|
||||
// }
|
||||
// 默认渲染
|
||||
td.textContent = value || ''
|
||||
|
||||
// 获取所有数据
|
||||
const data = instance.getSourceData()
|
||||
|
||||
// 检查当前值是否重复(排除空值)
|
||||
if (value && value.toString().trim() !== '') {
|
||||
const duplicateCount = data.filter((item: any) => item.code === value).length
|
||||
if (duplicateCount > 1) {
|
||||
td.style.color = 'red'
|
||||
td.style.fontWeight = 'bold'
|
||||
} else {
|
||||
td.style.color = ''
|
||||
td.style.fontWeight = ''
|
||||
}
|
||||
}else{
|
||||
|
||||
// td.style.backgroundColor = '#ff4d4f20' // 淡红色背景
|
||||
}
|
||||
|
||||
return td
|
||||
}*/
|
||||
|
||||
// export const materialsColumns:any[] = [
|
||||
// // {type:'text',data:'sort',title:'⇅', renderer: manualRowMoveRenderer,
|
||||
// // width: 40,
|
||||
// // readOnly: true,
|
||||
// // editor: false,
|
||||
// // },
|
||||
// {type:'text',data:'sortOrder',title:'序号', allowInvalid: true},
|
||||
// {type:'text',data:'name',title:'名称', allowInvalid: true},
|
||||
// {type:'text',data:'code',title:'类别', allowInvalid: true,
|
||||
// renderer: 'db-dropdown',
|
||||
// // source 会在 category.vue 中动态设置
|
||||
// onAfterSelect: (instance: any, row: number, column: number, selectedValue: string, optData?: any) => {
|
||||
// console.log('选择后回调:', { row, column, selectedValue, optData })
|
||||
// // 可以在这里处理选择后的逻辑,例如:
|
||||
// // 1. 根据选中的类别自动填充其他字段
|
||||
// // 2. 触发相关数据更新
|
||||
// // 3. 验证或联动其他列
|
||||
// if (optData) {
|
||||
// // 如果需要根据选中的数据更新其他列,可以这样做:
|
||||
// // instance.setDataAtRowProp(row, 'otherField', optData.someValue, 'autofill')
|
||||
// }
|
||||
// // const data = {
|
||||
// // categoryTreeId: 5,
|
||||
// // categoryId: 1,
|
||||
// // sortOrder: 1
|
||||
// // }
|
||||
// }
|
||||
// },
|
||||
// ]
|
||||
export const rootColum: any[] = [
|
||||
{type:'text',data:'sortOrder',title:'序号', required: true, allowInvalid: true, className: 'htCenter'},
|
||||
{type:'text',data:'name',title:'名称', required: true, allowInvalid: true},
|
||||
{type:'text',data:'code',title:'类别', required: true, allowInvalid: true},
|
||||
{type:'text',data:'taxExclBaseCode',title:'除税基价代码', required: true, allowInvalid: true, className: 'htRight'},
|
||||
{type:'text',data:'taxInclBaseCode',title:'含税基价代码', required: true, allowInvalid: true, className: 'htRight'},
|
||||
{type:'text',data:'taxExclCompileCode',title:'除税编制代码', required: true, allowInvalid: true, className: 'htRight'},
|
||||
{type:'text',data:'taxInclCompileCode',title:'含税编制代码', required: true, allowInvalid: true, className: 'htRight'},
|
||||
]
|
||||
|
||||
export const materialsColumns:any[] = [
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'category',title:'类别',
|
||||
renderer: 'db-dropdown',
|
||||
source: ['人', '人机', '材', '机'],
|
||||
readOnly: true,
|
||||
},
|
||||
{type:'text',data:'priceExTax',title:'除税基价代码'},
|
||||
{type:'text',data:'priceInTax',title:'含税基价代码'},
|
||||
{type:'text',data:'priceExTaxComp',title:'除税编制代码'},
|
||||
{type:'text',data:'priceInTaxComp',title:'含税编制代码'},
|
||||
]
|
||||
|
||||
// 直接使用配置对象,DbTree 会自动创建 HierarchyContextMenuHandler
|
||||
export const contextMenuHandler = {
|
||||
rootKey: 'add-category',
|
||||
rootText: '添加工料机总类',
|
||||
onRootAdd: async (newNode: any, allRootNodes: any[]) => {
|
||||
console.log('添加工料机总类:', newNode, '所有根节点:', allRootNodes)
|
||||
|
||||
// 计算根节点数量作为新的 sortOrder
|
||||
const newSortOrder = allRootNodes.length + 1
|
||||
newNode.code = 'ROOT'
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('新节点 sortOrder:', newSortOrder)
|
||||
// TODO: 有BUG添加时list不出现
|
||||
// await createCategories({ name: newNode.label, code: newNode.code, sortOrder: newNode.sortOrder }).then(res=>{
|
||||
// if(res){
|
||||
// newNode.id = res
|
||||
// }
|
||||
// })
|
||||
},
|
||||
onRootDelete: async (node: any) => {
|
||||
console.log('删除工料机总类:', node)
|
||||
// 调用 API 删除根节点
|
||||
await deleteCategories(node.id)
|
||||
},
|
||||
levels: [
|
||||
{
|
||||
depth: 0,
|
||||
addKey: 'add-province',
|
||||
addText: '添加省市',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加省市 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 检查父节点ID是否有效(不是临时ID)
|
||||
if (parentNode.id.includes('-')) {
|
||||
console.error('父节点未保存,无法添加子节点')
|
||||
throw new Error('请先保存父节点')
|
||||
}
|
||||
|
||||
// 计算子节点数量作为新的 sortOrder
|
||||
const newSortOrder = allChildren.length + 1
|
||||
|
||||
console.log('父节点下子节点数量:', allChildren.length, '新 sortOrder:', newSortOrder)
|
||||
|
||||
// 调用 API 保存省市节点 - 直接使用字符串ID
|
||||
// 生成唯一的编码:region + 随机字符串
|
||||
const uniqueCode = 'region-' + generateRandomCode()
|
||||
|
||||
await createCategoriesTree({
|
||||
parentId: parentNode.id, // 直接使用字符串ID
|
||||
code: uniqueCode,
|
||||
name: newNode.label,
|
||||
nodeType: "region",
|
||||
sortOrder: newSortOrder
|
||||
}).then(res => {
|
||||
if(res){
|
||||
newNode.id = String(res)
|
||||
}
|
||||
})
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除省市:', node)
|
||||
// 调用 API 删除省市节点
|
||||
await deleteCategoriesTree(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
addKey: 'add-specialty',
|
||||
addText: '添加工料机专业',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加工料机专业 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
// 检查父节点ID是否有效(不是临时ID)
|
||||
if (parentNode.id.includes('-')) {
|
||||
console.error('父节点未保存,无法添加子节点')
|
||||
throw new Error('请先保存父节点')
|
||||
}
|
||||
|
||||
// 计算子节点数量作为新的 sortOrder
|
||||
const newSortOrder = allChildren.length + 1
|
||||
|
||||
console.log('父节点下子节点数量:', allChildren.length, '新 sortOrder:', newSortOrder)
|
||||
|
||||
// 调用 API 保存工料机专业节点 - 直接使用字符串ID
|
||||
// 生成唯一的编码:specialty + 随机字符串
|
||||
const uniqueCode = 'specialty-' + generateRandomCode()
|
||||
|
||||
await createCategoriesTree({
|
||||
parentId: parentNode.id, // 直接使用字符串ID
|
||||
code: uniqueCode,
|
||||
name: newNode.label,
|
||||
nodeType: "specialty",
|
||||
sortOrder: newSortOrder
|
||||
}).then(res=>{
|
||||
if(res){
|
||||
newNode.id = String(res)
|
||||
}
|
||||
console.log(res)
|
||||
})
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除工料机专业:', node)
|
||||
// 调用 API 删除工料机专业节点
|
||||
await deleteCategoriesTree(node.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
allowDelete: true,
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除第三层节点:', node)
|
||||
// TODO: 调用 API 删除第三层节点
|
||||
await deleteCategoriesTree(node.id)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
export const createCatalogueMenuHandler = (getCategoryTreeId: () => number | string | null) => ({
|
||||
rootKey: 'add-catalogue',
|
||||
rootText: '添加目录',
|
||||
onRootAdd: async (newNode: any, allRootNodes: any[]) => {
|
||||
console.log('添加目录:', newNode, '所有根节点:', allRootNodes)
|
||||
|
||||
const categoryTreeId = getCategoryTreeId()
|
||||
if (!categoryTreeId) {
|
||||
ElMessage.error('请先选择工料机专业')
|
||||
throw new Error('categoryTreeId 未提供')
|
||||
}
|
||||
|
||||
// 计算根节点数量作为新的 sortOrder
|
||||
const newSortOrder = allRootNodes.length + 1
|
||||
|
||||
// 生成 code
|
||||
const code = newNode.label + generateRandomCode()
|
||||
|
||||
try {
|
||||
// 调用API创建根目录节点
|
||||
const id = await createCatalogItem({
|
||||
categoryTreeId: categoryTreeId,
|
||||
code: code,
|
||||
name: newNode.label,
|
||||
parentId: null,
|
||||
sortOrder: newSortOrder
|
||||
})
|
||||
|
||||
// 更新节点数据
|
||||
newNode.id = String(id)
|
||||
newNode.code = code
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('目录创建成功,新ID:', id)
|
||||
ElMessage.success('添加成功')
|
||||
} catch (error) {
|
||||
console.error('添加目录失败:', error)
|
||||
ElMessage.error('添加失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
onRootDelete: async (node: any) => {
|
||||
console.log('删除目录:', node)
|
||||
|
||||
try {
|
||||
await deleteCatalogItem(node.id)
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
console.error('删除目录失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
levels: [
|
||||
{
|
||||
depth: 0,
|
||||
addKey: 'add-material',
|
||||
addText: '添加子目录',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加子目录 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
const categoryTreeId = getCategoryTreeId()
|
||||
if (!categoryTreeId) {
|
||||
ElMessage.error('请先选择工料机专业')
|
||||
throw new Error('categoryTreeId 未提供')
|
||||
}
|
||||
|
||||
// 检查父节点ID是否有效(不是临时ID)
|
||||
if (parentNode.id.includes('-')) {
|
||||
ElMessage.error('请先保存父节点')
|
||||
throw new Error('父节点未保存')
|
||||
}
|
||||
|
||||
// 计算子节点数量作为新的 sortOrder
|
||||
const newSortOrder = allChildren.length + 1
|
||||
|
||||
// 生成 code
|
||||
const code = newNode.label + generateRandomCode()
|
||||
|
||||
console.log('父节点下子节点数量:', allChildren.length, '新 sortOrder:', newSortOrder)
|
||||
|
||||
try {
|
||||
// 调用API创建子目录节点
|
||||
const id = await createCatalogItem({
|
||||
categoryTreeId: categoryTreeId,
|
||||
code: code,
|
||||
name: newNode.label,
|
||||
parentId: parentNode.id, // 直接使用字符串ID
|
||||
sortOrder: newSortOrder
|
||||
})
|
||||
|
||||
// 更新节点数据
|
||||
newNode.id = String(id)
|
||||
newNode.code = code
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('子目录创建成功,新ID:', id)
|
||||
ElMessage.success('添加成功')
|
||||
} catch (error) {
|
||||
console.error('添加子目录失败:', error)
|
||||
ElMessage.error('添加失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除目录:', node)
|
||||
|
||||
try {
|
||||
await deleteCatalogItem(node.id)
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
console.error('删除目录失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
addKey: 'add-material-child',
|
||||
addText: '添加材料',
|
||||
sort: 'sortOrder',
|
||||
allowDelete: true,
|
||||
onAdd: async (parentNode: any, newNode: any, allChildren: any[]) => {
|
||||
console.log('添加材料 - 父节点:', parentNode, '新节点:', newNode, '所有子节点:', allChildren)
|
||||
|
||||
const categoryTreeId = getCategoryTreeId()
|
||||
if (!categoryTreeId) {
|
||||
ElMessage.error('请先选择工料机专业')
|
||||
throw new Error('categoryTreeId 未提供')
|
||||
}
|
||||
|
||||
// 检查父节点ID是否有效(不是临时ID)
|
||||
if (parentNode.id.includes('-')) {
|
||||
ElMessage.error('请先保存父节点')
|
||||
throw new Error('父节点未保存')
|
||||
}
|
||||
|
||||
// 计算子节点数量作为新的 sortOrder
|
||||
const newSortOrder = allChildren.length + 1
|
||||
|
||||
// 生成 code
|
||||
const code = newNode.label + generateRandomCode()
|
||||
|
||||
console.log('父节点下子节点数量:', allChildren.length, '新 sortOrder:', newSortOrder)
|
||||
|
||||
try {
|
||||
// 调用API创建材料节点
|
||||
const id = await createCatalogItem({
|
||||
categoryTreeId: categoryTreeId,
|
||||
code: code,
|
||||
name: newNode.label,
|
||||
parentId: parentNode.id, // 直接使用字符串ID
|
||||
sortOrder: newSortOrder
|
||||
})
|
||||
|
||||
// 更新节点数据
|
||||
newNode.id = String(id)
|
||||
newNode.code = code
|
||||
newNode.sortOrder = newSortOrder
|
||||
|
||||
console.log('材料创建成功,新ID:', id)
|
||||
ElMessage.success('添加成功')
|
||||
} catch (error) {
|
||||
console.error('添加材料失败:', error)
|
||||
ElMessage.error('添加失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除材料:', node)
|
||||
|
||||
try {
|
||||
await deleteCatalogItem(node.id)
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
console.error('删除材料失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
allowDelete: true,
|
||||
onDelete: async (node: any) => {
|
||||
console.log('删除目录:', node)
|
||||
|
||||
try {
|
||||
await deleteCatalogItem(node.id)
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
console.error('删除目录失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -1,81 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch } from 'vue'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElSplitter,ElSplitterPanel,ElCard,ElScrollbar } from 'element-plus';
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import {
|
||||
createCategoryTreeMapping,
|
||||
deleteCategoryTreeMapping,
|
||||
getCatalogsTreeList, getCategoriesTree,
|
||||
updateCategoriesTree
|
||||
} from '#/api/database/materials/index';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
// import { materialsColumns } from './category'
|
||||
import { manualRowMoveRenderer } from '#/components/db-hst/sort'
|
||||
|
||||
import { validatorRow } from '#/components/db-hst/validator';
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { ElCard, ElMessage, ElSplitter, ElSplitterPanel } from 'element-plus';
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { contextMenuHandler, rootColum, transformTreeData } from './category';
|
||||
|
||||
import { createCategories, deleteCategories, getCategoriesList, updateCategories } from '#/api/database/materials/root';
|
||||
//获取元素高度
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const { height: containerHeight } = useElementSize(containerRef)
|
||||
type Tree = { id: string; label: string; children?: Tree[] }
|
||||
|
||||
const treeData = ref<Tree[]>([
|
||||
{
|
||||
id: '1',
|
||||
label: '工料机总类',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
label: '广东',
|
||||
children: [
|
||||
{ id: '3', label: '广东工民建工料机' },
|
||||
{ id: '4', label: '广东公路工料机' },
|
||||
{ id: '5', label: '广东水利工料机' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '工料机总类2',
|
||||
children: [
|
||||
{
|
||||
id: '12',
|
||||
label: '广西',
|
||||
children: [
|
||||
{ id: '13', label: '工料机1' },
|
||||
{ id: '14', label: '工料机1' },
|
||||
{ id: '15', label: '工料机2' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const treeData = ref<Tree[]>([])
|
||||
const dbHstRef = ref<any>(null)
|
||||
const handleSelect = (node: Tree) => {
|
||||
if (dbHstRef.value && typeof dbHstRef.value.loadData === 'function') {
|
||||
// console.log('hstData.value',hstData.value)
|
||||
// dbHstRef.value.loadData(dbHstRef.value.defaultData(hstData.value))
|
||||
}
|
||||
}
|
||||
|
||||
// 直接使用配置对象,DbTree 会自动创建 HierarchyContextMenuHandler
|
||||
const contextMenuHandler = {
|
||||
rootKey: 'add-category',
|
||||
rootText: '添加总类',
|
||||
levels: [
|
||||
{
|
||||
depth: 0,
|
||||
addKey: 'add-province',
|
||||
addText: '添加省市',
|
||||
allowDelete: true
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
addKey: 'add-specialty',
|
||||
addText: '添加工料机专业',
|
||||
allowDelete: true
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
allowDelete: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const currentNode = ref<any>(null)
|
||||
|
||||
const materialsColumns:any[] = [
|
||||
// {type:'text',data:'sort',title:'⇅', renderer: manualRowMoveRenderer,
|
||||
@@ -83,55 +33,304 @@ const materialsColumns:any[] = [
|
||||
// readOnly: true,
|
||||
// editor: false,
|
||||
// },
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'category',title:'类别',
|
||||
{type:'text',data:'sortOrder',title:'序号',readOnly: true, allowInvalid: true, className: 'htCenter'},
|
||||
{type:'text',data:'name',title:'名称',readOnly: true, allowInvalid: true},
|
||||
{type:'text',data:'code',title:'类别', allowInvalid: true,
|
||||
renderer: 'db-dropdown',
|
||||
source: ['人', '人机', '材', '机'],
|
||||
readOnly: true,
|
||||
},
|
||||
{type:'text',data:'priceExTax',title:'除税基价代码'},
|
||||
{type:'text',data:'priceInTax',title:'含税基价代码'},
|
||||
{type:'text',data:'priceExTaxComp',title:'除税编制代码'},
|
||||
{type:'text',data:'priceInTaxComp',title:'含税编制代码'},
|
||||
]
|
||||
let i = 0
|
||||
const emptyRow = materialsColumns.reduce((acc, col) => {
|
||||
acc[col.data] = ''
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
const test = Array.from({ length: 30 }, () => ({ ...emptyRow }));
|
||||
test.forEach(e=>{
|
||||
e.code = i++
|
||||
})
|
||||
isOnlySelect: true,
|
||||
// source 会在 category.vue 中动态设置
|
||||
onAfterSelect: (instance: any, row: number, column: number, oldValue: any, newValue: string, optData?: any) => {
|
||||
console.log('选择后回调:', { row, column, oldValue, newValue, optData })
|
||||
|
||||
let dbSettings = {
|
||||
columns: materialsColumns,
|
||||
data: test,
|
||||
// manualRowMove: true,
|
||||
if(newValue == oldValue){
|
||||
return
|
||||
}
|
||||
|
||||
const sortOrder = Math.max(...instance.getSourceData().map(item => Number(item.sortOrder ?? 0))) + 1
|
||||
const data = {
|
||||
categoryTreeId: currentNode.value.id,
|
||||
categoryId: optData.id,
|
||||
sortOrder: sortOrder
|
||||
}
|
||||
createCategoryTreeMapping(data).then(res=>{
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
instance.setDataAtRowProp([
|
||||
[row, 'id', optData.id],
|
||||
[row, 'sortOrder', rowData.sortOrder ?? sortOrder ],
|
||||
[row, 'name', optData.name],
|
||||
// [row, 'code', selectedValue]
|
||||
],'createMapping')
|
||||
ElMessage.success('绑定成功')
|
||||
}).catch(err => {
|
||||
console.error('绑定失败', err)
|
||||
ElMessage.error('绑定失败')
|
||||
})
|
||||
console.log(data)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let rootSettings = {
|
||||
columns: rootColum,
|
||||
// startRows: 1,
|
||||
data: [],
|
||||
// manualRowMove: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
console.log('删除工料机类别字典:', rowData)
|
||||
if (rowData?.id) {
|
||||
deleteCategories(rowData.id).then(res => {
|
||||
console.log('deleteCategories 成功')
|
||||
ElMessage.success('删除成功')
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}).catch(err => {
|
||||
console.error('删除失败', err)
|
||||
})
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
async afterChange(changes, source) {
|
||||
console.log('source',source)
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'createMapping') return
|
||||
// 如果有空单元格,提前返回,不执行后续操作
|
||||
if(!validatorRow(this, changes)){
|
||||
return
|
||||
}
|
||||
|
||||
const row = changes[0][0]; // 获取行索引
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
if (rowData.id == null) {
|
||||
console.log('创建工料机类别字典', rowData )
|
||||
// 调用创建接口 - 使用工料机类别字典接口
|
||||
createCategories(rowData).then(res => {
|
||||
console.log('createCategories 成功', res)
|
||||
// 更新当前行ID - 使用自定义source避免循环
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId');
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
// 调用更新接口 - 使用工料机类别字典接口
|
||||
console.log('更新工料机类别字典', rowData)
|
||||
updateCategories(rowData).then(res => {
|
||||
console.log('updateCategories 成功', res)
|
||||
ElMessage.success('更新成功')
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let treeSettings = {
|
||||
columns: materialsColumns,
|
||||
// startRows: 1,
|
||||
data: [],
|
||||
// manualRowMove: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
console.log(currentNode.value.id,rowData.id)
|
||||
if (rowData?.id) {
|
||||
deleteCategoryTreeMapping(currentNode.value.id,rowData.id).then(res => {
|
||||
console.log('deleteCategoryTreeMapping')
|
||||
ElMessage.success('删除成功')
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}).catch(err => {
|
||||
console.error('删除失败', err)
|
||||
ElMessage.error('删除失败')
|
||||
})
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
//需要重置
|
||||
afterChange(changes, source) {
|
||||
}
|
||||
}
|
||||
let dbSettings = {
|
||||
...rootSettings
|
||||
}
|
||||
|
||||
const handleSelect = (node: any) => {
|
||||
if (dbHstRef.value && typeof dbHstRef.value.loadData === 'function') {
|
||||
console.log('handleSelect',node)
|
||||
currentNode.value = node
|
||||
|
||||
// 检查是否是临时ID(包含'-')
|
||||
if (node.id.includes('-')) {
|
||||
console.warn('临时节点,无法加载数据:', node.id)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是省市节点(非ROOT),显示空白页并禁用右键菜单
|
||||
if(node.nodeType == "region" && node.code != "ROOT"){
|
||||
contextMenuItems.value = [] // 清空自定义右键菜单
|
||||
const emptySettings = {
|
||||
columns: [],
|
||||
data: [],
|
||||
contextMenu: false, // 禁用右键菜单
|
||||
readOnly: true
|
||||
}
|
||||
dbHstRef?.value.hotInstance.updateSettings(emptySettings)
|
||||
dbHstRef.value.loadData([])
|
||||
dbHstRef.value.hotInstance.render()
|
||||
return
|
||||
}
|
||||
|
||||
if(node.nodeType == "region" && node.code == "ROOT"){
|
||||
// 恢复自定义右键菜单
|
||||
contextMenuItems.value = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
if(currentNode.value == null){
|
||||
ElMessage.error("请选择节点");
|
||||
return
|
||||
}
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
dbHstRef?.value.hotInstance.updateSettings(rootSettings)
|
||||
dbHstRef.value.hotInstance.render()
|
||||
getCategoriesList().then(res=>{
|
||||
console.log('region:', res)
|
||||
|
||||
dbHstRef.value.loadData(res)
|
||||
|
||||
})
|
||||
}else {
|
||||
// 恢复自定义右键菜单
|
||||
contextMenuItems.value = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
if(currentNode.value == null){
|
||||
ElMessage.error("请选择节点");
|
||||
return
|
||||
}
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
dbHstRef?.value.hotInstance.updateSettings(treeSettings)
|
||||
dbHstRef.value.hotInstance.render()
|
||||
getCategoriesTree(node.id).then(res=>{
|
||||
console.log('getCategories:', res)
|
||||
dbHstRef.value.loadData(res)
|
||||
dbHstRef.value.hotInstance.render()
|
||||
})
|
||||
setTimeout(() => {
|
||||
getCategoriesList().then(res=>{
|
||||
//{id: 5, code: "设", name: "设备费", sortOrder: 5}
|
||||
//res.data
|
||||
const transformedData = res.map((item: any) => ({
|
||||
value: item.code,
|
||||
label: item.code,
|
||||
data: item
|
||||
}))
|
||||
// catalogsList.value = transformedData
|
||||
console.log('getCatalogItemsAllowedList',transformedData)
|
||||
|
||||
// 更新 topHstRef 的 type 列的 source
|
||||
if (dbHstRef.value?.hotInstance) {
|
||||
const typeColIndex = materialsColumns.findIndex((col: any) => col.data === 'code')
|
||||
if (typeColIndex !== -1) {
|
||||
const columns = dbHstRef.value.hotInstance.getSettings().columns
|
||||
columns[typeColIndex].source = transformedData
|
||||
dbHstRef.value.hotInstance.updateSettings({
|
||||
columns: columns
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditSave = async (node: any, oldLabel: string, newLabel: string) => {
|
||||
console.log('节点编辑保存:', { node, oldLabel, newLabel })
|
||||
|
||||
// 处理省市节点和工料机专业节点的更新
|
||||
if(node.nodeType == "region" || node.nodeType == "specialty"){
|
||||
const {id,parentId,code,nodeType,sortOrder} = node
|
||||
await updateCategoriesTree({id,parentId,code,name:newLabel,nodeType,sortOrder}).then(res=>{
|
||||
ElMessage.success('更新成功')
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
ElMessage.error('更新失败')
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
const contextMenuItems = ref([
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
if(currentNode.value == null){
|
||||
ElMessage.error("请选择节点");
|
||||
return
|
||||
}
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
// const loadingPlugin = ref()
|
||||
onMounted(() => {
|
||||
getCatalogsTreeList().then(res=>{
|
||||
console.log('原始数据:', res)
|
||||
treeData.value = transformTreeData(res)
|
||||
console.log('转换后数据:', treeData.value)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('containerHeight.value',containerHeight.value)
|
||||
// console.log('containerHeight.value',containerHeight.value)
|
||||
dbHstRef?.value.hotInstance.updateSettings({
|
||||
height: containerHeight.value,
|
||||
})
|
||||
|
||||
// 生成模拟数据
|
||||
const categories = ['人', '人机', '材', '机']
|
||||
const mockData = Array.from({ length: 300 }, (_, index) => ({
|
||||
code: `MAT${String(index + 1).padStart(6, '0')}`,
|
||||
name: `材料名称${index + 1}`,
|
||||
category: categories[index % categories.length],
|
||||
priceExTax: `EX${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
|
||||
priceInTax: `IN${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
|
||||
priceExTaxComp: `EXCOMP${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
|
||||
priceInTaxComp: `INCOMP${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
|
||||
}))
|
||||
|
||||
dbHstRef?.value.loadData(mockData)
|
||||
dbHstRef?.value.hotInstance.render()
|
||||
|
||||
// loadingPlugin.value = dbHstRef?.value.hotInstance.getPlugin('loading');
|
||||
// loadingPlugin.value.show();
|
||||
// loadingPlugin.value.hide();
|
||||
// dbHstRef?.value.loadData(mockData)
|
||||
// dbHstRef?.value.hotInstance.render()
|
||||
|
||||
}, 200);
|
||||
})
|
||||
onUnmounted(() => {})
|
||||
@@ -142,12 +341,13 @@ onUnmounted(() => {})
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="treeData" @select="handleSelect" :defaultExpandedKeys="2" :context-menu-handler="contextMenuHandler" :search="false" />
|
||||
<DbTree :height="containerHeight" :data="treeData" @select="handleSelect" @edit-save="handleEditSave"
|
||||
:defaultExpandedKeys="2" :context-menu-handler="contextMenuHandler" :search="false" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full" body-class="!p-0 h-full" >
|
||||
<DbHst ref="dbHstRef" :settings="dbSettings" class=""></DbHst>
|
||||
<ElCard class="w-full h-full" body-class="!p-0 h-full">
|
||||
<DbHst ref="dbHstRef" :settings="dbSettings" :contextMenuItems="contextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { deleteResourceMerged, getResourceItemsPage, updateResourceMerged } from '#/api/database/materials/index'
|
||||
import { DICT_TYPE } from '@vben/constants'
|
||||
import { getDictOptions } from '@vben/hooks'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nextTick, ref } from 'vue'
|
||||
// import { getResourceItemsPage, deleteResourceMerged } from '#/api/database/materials/index'
|
||||
|
||||
// 字典渲染器
|
||||
const dictRenderer = (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
const source = cellProperties.source || []
|
||||
const option = source.find((item: any) => item.value === value)
|
||||
td.innerHTML = option?.label || value || ''
|
||||
td.style.textAlign = 'center'
|
||||
return td
|
||||
}
|
||||
export const useBottomTable = (mergedId: any) => {
|
||||
const bottomHstRef = ref<any>(null)
|
||||
|
||||
// 处理定额消耗量失去焦点事件
|
||||
const handleQuotaConsumptionBlur = async (row: number, col: number, hotInstance: any) => {
|
||||
const rowData = hotInstance.getSourceDataAtRow(row)
|
||||
|
||||
// 只有当行数据存在ID时才更新(已保存的数据)
|
||||
if (!rowData?.id) {
|
||||
ElMessage.warning('请先保存数据后再修改定额消耗量')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必要字段
|
||||
if (!rowData.sourceId) {
|
||||
ElMessage.error('数据源ID不存在,无法更新')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取定额消耗量的值
|
||||
const quotaConsumption = rowData.quotaConsumption
|
||||
|
||||
try {
|
||||
// 调用更新接口,只更新定额消耗量字段
|
||||
await updateResourceMerged({
|
||||
id: rowData.id,
|
||||
mergedId: mergedId.value,
|
||||
sourceId: rowData.sourceId,
|
||||
quotaConsumption: quotaConsumption
|
||||
})
|
||||
|
||||
ElMessage.success('定额消耗量更新成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('定额消耗量更新失败')
|
||||
console.error('更新定额消耗量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const bottomColumns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码', readOnly: true },
|
||||
{ type: 'text', data: 'name', title: '名称', readOnly: true},
|
||||
{ type: 'text', data: 'spec', title: '型号规格', readOnly: true },
|
||||
{ type: 'text', data: 'categoryName', title: '类别', readOnly: true },
|
||||
{ type: 'text', data: 'unit', title: '单位', readOnly: true,
|
||||
renderer: dictRenderer, source: getDictOptions(DICT_TYPE.MATERIAL_UNIT, 'string')
|
||||
},
|
||||
{ type: 'numeric', data: 'taxRate', title: '税率', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxExclBasePrice', title: '除税基价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxInclBasePrice', title: '含税基价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxExclCompilePrice', title: '除税编制价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxInclCompilePrice', title: '含税编制价', readOnly: true },
|
||||
{ type: 'numeric', data: 'quotaConsumption', title: '定额消耗量' },
|
||||
{ type: 'numeric', data: 'taxExclBaseTotal', title: '除税基价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxInclBaseTotal', title: '含税基价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxExclCompileTotal', title: '除税编制价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'taxInclCompileTotal', title: '含税编制价合价', readOnly: true },
|
||||
])
|
||||
|
||||
const bottomContextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
if (mergedId.value == null) {
|
||||
ElMessage.error("请选择上方某一行")
|
||||
return
|
||||
}
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const bottomDbSettings = {
|
||||
data: [],
|
||||
columns: bottomColumns.value,
|
||||
colWidths: 110,
|
||||
afterChange: (changes: any, source: string) => {
|
||||
// 只处理用户编辑触发的变更
|
||||
if (source === 'edit' && changes) {
|
||||
changes.forEach((change: any) => {
|
||||
const [row, prop, oldValue, newValue] = change
|
||||
|
||||
// 只处理定额消耗量字段的变更
|
||||
if (prop === 'quotaConsumption' && oldValue !== newValue) {
|
||||
const hotInstance = bottomHstRef.value?.hotInstance
|
||||
if (hotInstance) {
|
||||
handleQuotaConsumptionBlur(row, 0, hotInstance)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
this.alter('insert_row_above', selectedRow, 1)
|
||||
},
|
||||
},
|
||||
row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
this.alter('insert_row_below', selectedRow, 1)
|
||||
},
|
||||
},
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
deleteResourceMerged(rowData.id).then(res => {
|
||||
console.log('deleteResourceMerged', res)
|
||||
})
|
||||
} else {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async (mergedIdValue: number) => {
|
||||
if (!mergedIdValue) {
|
||||
bottomHstRef.value?.loadData([])
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getResourceItemsPage({ mergedId: mergedIdValue })
|
||||
bottomHstRef.value?.loadData(res.list)
|
||||
bottomHstRef.value?.hotInstance?.render()
|
||||
return res
|
||||
}
|
||||
|
||||
const updateHeight = (height: number) => {
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
bottomHstRef.value.hotInstance.updateSettings({ height })
|
||||
}
|
||||
}
|
||||
|
||||
const updateNameColumn = (selectRender: any, callbacks: any) => {
|
||||
bottomColumns.value = bottomColumns.value.map(col =>
|
||||
col.data === 'name'
|
||||
? { ...col, renderer: selectRender, ...callbacks }
|
||||
: col
|
||||
)
|
||||
|
||||
bottomDbSettings.columns = bottomColumns.value
|
||||
}
|
||||
|
||||
return {
|
||||
bottomHstRef,
|
||||
bottomColumns,
|
||||
bottomDbSettings,
|
||||
bottomContextMenuItems,
|
||||
loadData,
|
||||
updateHeight,
|
||||
updateNameColumn,
|
||||
handleQuotaConsumptionBlur
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useCalcDialog = (hotInstanceRef: any) => {
|
||||
const calcDialogVisible = ref(false)
|
||||
const currentRow = ref<number>(0)
|
||||
const currentCalcValue = ref<string>('')
|
||||
|
||||
const openDialog = (row: number) => {
|
||||
currentRow.value = row
|
||||
|
||||
if (hotInstanceRef.value?.hotInstance) {
|
||||
const currentValue = hotInstanceRef.value.hotInstance.getDataAtRowProp(row, 'calcBase')
|
||||
if (currentValue && typeof currentValue === 'object' && currentValue.formula) {
|
||||
currentCalcValue.value = currentValue.formula
|
||||
} else if (typeof currentValue === 'string') {
|
||||
currentCalcValue.value = currentValue
|
||||
} else {
|
||||
currentCalcValue.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
calcDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleConfirm = (result: { formula: string; variables: Record<string, number | { categoryId: number; priceField: string }> }) => {
|
||||
if (hotInstanceRef.value?.hotInstance && result.formula) {
|
||||
hotInstanceRef.value.hotInstance.setDataAtRowProp([
|
||||
[currentRow.value, 'calcBase', result],
|
||||
[currentRow.value, 'calcBase.formula', result.formula]
|
||||
])
|
||||
hotInstanceRef.value.hotInstance.render()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
calcDialogVisible,
|
||||
currentCalcValue,
|
||||
openDialog,
|
||||
handleConfirm
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ref } from 'vue'
|
||||
import { getCatalogsTreeList, getCatalogItemsAllowedList } from '#/api/database/materials/index'
|
||||
import { transformTreeData } from '../category'
|
||||
|
||||
export const useCategoryTree = () => {
|
||||
const categoryTreeData = ref<any[]>([])
|
||||
const catalogsTreeId = ref<string>()
|
||||
|
||||
const loadCategoryTree = async () => {
|
||||
const res = await getCatalogsTreeList()
|
||||
categoryTreeData.value = transformTreeData(res)
|
||||
return res
|
||||
}
|
||||
|
||||
const handleSelect = async (node: any) => {
|
||||
catalogsTreeId.value = node.id
|
||||
const res = await getCatalogItemsAllowedList(node.id)
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
categoryTreeData,
|
||||
catalogsTreeId,
|
||||
loadCategoryTree,
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { getCatalogsTreeItems, updateCatalogItem } from '#/api/database/materials/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
import { createCatalogueMenuHandler, transformTreeData } from '../category'
|
||||
|
||||
export const useDetailTree = (catalogsTreeId: any) => {
|
||||
const detailTreeData = ref<any[]>([])
|
||||
const catalogItemId = ref<number | null>()
|
||||
// const catalogsTreeId = ref<string>()
|
||||
|
||||
const catalogueMenuHandler = createCatalogueMenuHandler(()=>catalogsTreeId.value)
|
||||
|
||||
const loadDetailTree = async (catalogId: string) => {
|
||||
const res = await getCatalogsTreeItems(catalogId)
|
||||
detailTreeData.value = transformTreeData(res)
|
||||
return res
|
||||
}
|
||||
|
||||
const handleSelect = (node: any) => {
|
||||
catalogItemId.value = parseInt(node.id)
|
||||
return catalogItemId.value
|
||||
}
|
||||
|
||||
const handleEditSave = async (node: any, oldLabel: string, newLabel: string) => {
|
||||
console.log('保存节点名称修改 - 完整节点数据:', node)
|
||||
console.log('旧名称:', oldLabel, '新名称:', newLabel)
|
||||
|
||||
// 检查节点ID是否有效(不是临时ID)
|
||||
if (node.id.includes('-')) {
|
||||
ElMessage.error('请先保存节点')
|
||||
throw new Error('节点未保存')
|
||||
}
|
||||
|
||||
// 确保 code 字段存在,如果不存在则使用 name 或 label
|
||||
const code = node.code || node.name || node.label || newLabel
|
||||
|
||||
// 从 path 数组中提取 parentPath(去掉最后一个元素,即当前节点的 code)
|
||||
let parentPath: string[] | undefined = undefined
|
||||
if (node.path && Array.isArray(node.path) && node.path.length > 1) {
|
||||
parentPath = node.path.slice(0, -1)
|
||||
}
|
||||
|
||||
console.log('计算的 parentPath:', parentPath)
|
||||
|
||||
try {
|
||||
// 调用API更新节点名称,使用 parentPath 而不是 parentId
|
||||
await updateCatalogItem({
|
||||
id: node.id,
|
||||
categoryTreeId: catalogsTreeId.value,
|
||||
code: code,
|
||||
name: newLabel,
|
||||
unit: node.unit,
|
||||
parentPath: parentPath,
|
||||
sortOrder: node.sortOrder,
|
||||
attributes: node.attributes
|
||||
})
|
||||
|
||||
ElMessage.success('修改成功')
|
||||
} catch (error) {
|
||||
console.error('修改节点名称失败:', error)
|
||||
ElMessage.error('修改失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
detailTreeData,
|
||||
catalogItemId,
|
||||
catalogsTreeId,
|
||||
catalogueMenuHandler,
|
||||
loadDetailTree,
|
||||
handleSelect,
|
||||
handleEditSave
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { createResourceMerged, getCatalogItemsPage, updateResourceMerged } from '#/api/database/materials/index'
|
||||
import { createPopoverCellRenderer, usePopoverClickOutside } from '#/components/db-hst/popover'
|
||||
import { DICT_TYPE } from '@vben/constants'
|
||||
import { getDictOptions } from '@vben/hooks'
|
||||
import type { PopoverInstance, RowEventHandlers } from 'element-plus'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
|
||||
export const useItemSelect = (mergedId: any, catalogItemId: any) => {
|
||||
const visible = ref(false)
|
||||
const buttonRef = ref<HTMLElement | null>()
|
||||
const popoverRef = ref<PopoverInstance>()
|
||||
const popoverTableData = ref<any[]>([])
|
||||
const itemSelectPopover = ref<any>()
|
||||
|
||||
const popoverPagination = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
loading: false,
|
||||
hasMore: true
|
||||
})
|
||||
|
||||
const unitLabelByValue = computed(() => new Map(getDictOptions(DICT_TYPE.MATERIAL_UNIT, 'string').map((option) => [option.value, option.label])))
|
||||
|
||||
const selectRender = createPopoverCellRenderer({
|
||||
visible,
|
||||
buttonRef,
|
||||
})
|
||||
|
||||
const columnsStyle = {
|
||||
borderLeft: '1px solid #e4e4e7',
|
||||
background: '#f0f0f0',
|
||||
color: '#373737',
|
||||
fontWeight: 400
|
||||
}
|
||||
|
||||
const popoverColumns = computed(() => [
|
||||
{ key: 'code', dataKey: 'code', title: '编码', width: 120 },
|
||||
{ key: 'name', dataKey: 'name', title: '名称', width: 150 },
|
||||
{ key: 'spec', dataKey: 'spec', title: '型号规格', width: 120 },
|
||||
{ key: 'type', dataKey: 'type', title: '类别', width: 60, align: 'center' },
|
||||
{ key: 'unit', dataKey: 'unit', title: '单位', width: 60, align: 'center', cellRenderer: ({ cellData }: { cellData: any }) => {
|
||||
const label = cellData == null ? '' : unitLabelByValue.value.get(cellData) ?? String(cellData)
|
||||
return h('span', label)
|
||||
}, },
|
||||
{ key: 'taxRate', dataKey: 'taxRate', title: '税率', width: 60, align: 'right' },
|
||||
{ key: 'taxExclBasePrice', dataKey: 'taxExclBasePrice', title: '除税基价', width: 90, align: 'right' },
|
||||
{ key: 'taxInclBasePrice', dataKey: 'taxInclBasePrice', title: '含税基价', width: 90, align: 'right' },
|
||||
{ key: 'taxExclCompilePrice', dataKey: 'taxExclCompilePrice', title: '除税编制价', width: 90, align: 'right' },
|
||||
{ key: 'taxInclCompilePrice', dataKey: 'taxInclCompilePrice', title: '含税编制价', width: 90, align: 'right' },
|
||||
].map(item => ({
|
||||
...item,
|
||||
style: {
|
||||
...columnsStyle,
|
||||
...(item.key === 'taxInclCompilePrice' ? { borderRight: '1px solid #e4e4e7' } : {})
|
||||
}
|
||||
})))
|
||||
|
||||
const onClickOutside = () => {
|
||||
popoverRef.value?.hide()
|
||||
}
|
||||
|
||||
usePopoverClickOutside(visible, onClickOutside)
|
||||
|
||||
const getTableDataList = async (query: any, append: boolean = false) => {
|
||||
if (popoverPagination.loading) return
|
||||
|
||||
popoverPagination.loading = true
|
||||
try {
|
||||
const res = await getCatalogItemsPage({
|
||||
...query,
|
||||
catalogItemId: catalogItemId.value, // 添加 catalogItemId 参数
|
||||
includeChildren: true, // 包含子节点
|
||||
pageNo: popoverPagination.pageNo,
|
||||
pageSize: popoverPagination.pageSize
|
||||
})
|
||||
|
||||
popoverPagination.total = res.total || 0
|
||||
popoverPagination.hasMore = (popoverPagination.pageNo * popoverPagination.pageSize) < popoverPagination.total
|
||||
|
||||
if (append) {
|
||||
popoverTableData.value = [...popoverTableData.value, ...(res.list || [])]
|
||||
} else {
|
||||
popoverTableData.value = res.list || []
|
||||
}
|
||||
|
||||
return res.list || []
|
||||
} finally {
|
||||
popoverPagination.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableEndReached = () => {
|
||||
if (popoverPagination.hasMore && !popoverPagination.loading) {
|
||||
popoverPagination.pageNo++
|
||||
getTableDataList({}, true)
|
||||
}
|
||||
}
|
||||
|
||||
const rowEventHandlers: RowEventHandlers = {
|
||||
onClick: ({ rowIndex, event, rowData }) => {
|
||||
event.stopPropagation()
|
||||
visible.value = false
|
||||
onClickOutside()
|
||||
|
||||
const { row, col, prop, value, currentRowData, instance, td } = itemSelectPopover.value
|
||||
|
||||
setTimeout(() => {
|
||||
instance.setDataAtRowProp([
|
||||
[row, 'code', rowData.code || ''],
|
||||
[row, 'name', rowData.name || ''],
|
||||
[row, 'spec', rowData.spec || ''],
|
||||
[row, 'type', rowData.type || ''],
|
||||
[row, 'unit', rowData.unit || ''],
|
||||
[row, 'categoryName', rowData.categoryName || ''],
|
||||
[row, 'taxRate', rowData.taxRate || ''],
|
||||
[row, 'taxExclBasePrice', rowData.taxExclBasePrice],
|
||||
[row, 'taxInclBasePrice', rowData.taxInclBasePrice],
|
||||
[row, 'taxExclCompilePrice', rowData.taxExclCompilePrice],
|
||||
[row, 'taxInclCompilePrice', rowData.taxInclCompilePrice],
|
||||
[row, 'mergedId', mergedId.value],
|
||||
[row, 'sourceId', rowData.id],
|
||||
])
|
||||
instance.render()
|
||||
console.log(rowData,{ mergedId: mergedId.value, sourceId: rowData.id })
|
||||
if (currentRowData.id == null) {
|
||||
createResourceMerged({ mergedId: mergedId.value, sourceId: rowData.id }).then(res => {
|
||||
instance.setDataAtRowProp([
|
||||
[row, 'id', res],
|
||||
],'updateId')
|
||||
})
|
||||
} else {
|
||||
updateResourceMerged({
|
||||
id: currentRowData.id,
|
||||
mergedId: mergedId.value,
|
||||
sourceId: rowData.id,
|
||||
quotaConsumption: currentRowData.quotaConsumption || null
|
||||
}).then(res => {
|
||||
console.log('updateResourceMerged', res)
|
||||
})
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
}
|
||||
|
||||
const createCallbacks = () => ({
|
||||
focusCallback: async (row: any, col: any, prop: any, value: any, rowData: any, instance: any, td: any) => {
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({}, false)
|
||||
itemSelectPopover.value = { row, col, prop, value, currentRowData: rowData, instance, td }
|
||||
},
|
||||
selectCallback: async (row: any, col: any, prop: any, value: any, rowData: any, instance: any, td: any) => {
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({}, false)
|
||||
itemSelectPopover.value = { row, col, prop, value, currentRowData: rowData, instance, td }
|
||||
},
|
||||
inputCallback: async (row: any, col: any, prop: any, value: any, rowData: any, instance: any, td: any) => {
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({ name: value.trim() }, false)
|
||||
itemSelectPopover.value = { row, col, prop, value, currentRowData: rowData, instance, td }
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
visible,
|
||||
buttonRef,
|
||||
popoverRef,
|
||||
popoverTableData,
|
||||
popoverColumns,
|
||||
popoverPagination,
|
||||
selectRender,
|
||||
rowEventHandlers,
|
||||
handleTableEndReached,
|
||||
createCallbacks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { createResourceItems, deleteResourceItems, getCatalogItemsAllowedList, getCatalogItemsPage, updateResourceItems } from '#/api/database/materials/index'
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
import { validatorRow } from '#/components/db-hst/validator'
|
||||
import { DICT_TYPE } from '@vben/constants'
|
||||
import { getDictOptions } from '@vben/hooks'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { nameRenderer } from '../machine'
|
||||
|
||||
export const useTopTable = (catalogItemId: any, mergedId: any, onMergedIdChange?: (id: number) => void) => {
|
||||
const topHstRef = ref<any>(null)
|
||||
const selectedRow = ref<any>(null)
|
||||
const calcTableData = ref<any[]>([])
|
||||
|
||||
const { load, codeRenderer, levelRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => topHstRef.value?.hotInstance,
|
||||
})
|
||||
|
||||
const topColumns:any[] = [
|
||||
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer, required: true, autoWidth: true },
|
||||
{ type: 'text', data: 'name', title: '名称', required: true, renderer: nameRenderer },
|
||||
{ type: 'text', data: 'spec', title: '型号规格', required: true },
|
||||
{ type: 'text', data: 'type', title: '类别', required: true, renderer: 'db-dropdown', source: [] },
|
||||
{ type: 'text', data: 'unit', title: '单位', required: true, renderer: 'db-dropdown', source: getDictOptions(DICT_TYPE.MATERIAL_UNIT, 'string') },
|
||||
{ type: 'numeric', data: 'taxRate', title: '税率', required: true },
|
||||
{ type: 'numeric', data: 'taxExclBasePrice', title: '除税基价代码' },
|
||||
{ type: 'numeric', data: 'taxInclBasePrice', title: '含税基价代码' },
|
||||
{ type: 'numeric', data: 'taxExclCompilePrice', title: '除税编制代码' },
|
||||
{ type: 'numeric', data: 'taxInclCompilePrice', title: '含税编制代码' },
|
||||
]
|
||||
|
||||
const topContextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
if (catalogItemId.value == null) {
|
||||
ElMessage.error("请选择")
|
||||
return
|
||||
}
|
||||
handleRowOperation(hotInstance, 'append')
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const topDbSettings = {
|
||||
data: [],
|
||||
dataSchema: initSchema(topColumns),
|
||||
colWidths: 100,
|
||||
columns: topColumns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
const selected = this.getSelected()
|
||||
const row = selected[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
handleRowOperation(this, 'delete')
|
||||
deleteResourceItems(rowData.id).then(res => {
|
||||
console.log('deleteResourceItems', res)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement) {
|
||||
if (event.button !== 0) return
|
||||
selectedRow.value = coords
|
||||
const rowObj = topHstRef.value.hotInstance.getSourceDataAtRow(coords.row)
|
||||
if (rowObj?.id) {
|
||||
mergedId.value = rowObj.id
|
||||
if (onMergedIdChange) {
|
||||
onMergedIdChange(rowObj.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
afterChange(changes, source) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return
|
||||
selectedRow.value = null
|
||||
// applyAutoFitColum(this, changes)
|
||||
|
||||
if (!validatorRow(this, changes)) return
|
||||
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
// 从 type 列获取对应的 categoryId
|
||||
// calcTableData 包含 {id, code, name} 的类别数据
|
||||
const selectedCategory = calcTableData.value.find((cat: any) => cat.code === rowData.type)
|
||||
const categoryId = selectedCategory?.id || null
|
||||
const sendData: any = {
|
||||
catalogItemId: catalogItemId.value,
|
||||
categoryId: categoryId,
|
||||
...rowData
|
||||
}
|
||||
|
||||
if (rowData.id == null) {
|
||||
createResourceItems(sendData).then(res => {
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateResourceItems(sendData).then(res => {
|
||||
console.log('updateResourceItems', res)
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
afterDeselect(){
|
||||
highlightDeselect(this, selectedRow.value)
|
||||
}
|
||||
}
|
||||
|
||||
const transformToHierarchicalData = (items: any[], parentLevel: string | null = null): any[] => {
|
||||
return items.map((item, index) => {
|
||||
//const currentLevel = parentLevel === null ? String(index) : `${parentLevel}-${index + 1}`
|
||||
|
||||
const transformedItem: any = {
|
||||
...item,
|
||||
id: item.id || null,
|
||||
level: null,
|
||||
__children: []
|
||||
|
||||
}
|
||||
|
||||
topColumns.forEach((col: any) => {
|
||||
if (col.data.includes('.')) {
|
||||
const keys = col.data.split('.')
|
||||
const rootKey = keys[0]
|
||||
transformedItem[rootKey] = item[rootKey] || {}
|
||||
let value = item
|
||||
for (const key of keys) {
|
||||
value = value?.[key]
|
||||
if (value === undefined || value === null) break
|
||||
}
|
||||
transformedItem[col.data] = value || null
|
||||
}
|
||||
})
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
transformedItem.__children = transformToHierarchicalData(item.children, null)
|
||||
}
|
||||
return transformedItem
|
||||
})
|
||||
}
|
||||
|
||||
const loadData = async (catalogId: number) => {
|
||||
catalogItemId.value = catalogId
|
||||
mergedId.value = null
|
||||
selectedRow.value = null
|
||||
topHstRef.value.hotInstance.deselectCell()
|
||||
// 清空底部表格数据
|
||||
if (onMergedIdChange) {
|
||||
onMergedIdChange(null as any)
|
||||
}
|
||||
|
||||
const res = await getCatalogItemsPage({ catalogItemId: catalogId, pageNo: 1, pageSize: 10 })
|
||||
|
||||
const hierarchicalData = transformToHierarchicalData(res.list || [])
|
||||
|
||||
load(hierarchicalData)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const loadAllowedCategories = async () => {
|
||||
const res = await getCatalogItemsAllowedList(catalogItemId.value)
|
||||
const transformedData = res.map((item: any) => ({
|
||||
id: item.id,
|
||||
code: item.code,
|
||||
name: item.name, // 添加 name 字段用于表格显示
|
||||
value: item.code,
|
||||
label: item.name
|
||||
}))
|
||||
calcTableData.value = transformedData
|
||||
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
const typeColIndex = topColumns.findIndex((col: any) => col.data === 'type')
|
||||
if (typeColIndex !== -1) {
|
||||
const columns = topHstRef.value.hotInstance.getSettings().columns
|
||||
columns[typeColIndex].source = transformedData
|
||||
topHstRef.value.hotInstance.updateSettings({ columns: columns })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateHeight = (height: number) => {
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.updateSettings({ height: height - 8 })
|
||||
topHstRef.value.updateCodeColWidth()
|
||||
}
|
||||
}
|
||||
const loadTopDataClear = ()=>{
|
||||
selectedRow.value = null
|
||||
topHstRef.value.hotInstance.deselectCell()
|
||||
load([])
|
||||
}
|
||||
return {
|
||||
topHstRef,
|
||||
topColumns,
|
||||
topDbSettings,
|
||||
topContextMenuItems,
|
||||
calcTableData,
|
||||
loadData,
|
||||
loadAllowedCategories,
|
||||
updateHeight,
|
||||
loadTopDataClear
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,107 @@
|
||||
export const topColHeaders: string[] = [
|
||||
'编码',
|
||||
'名称',
|
||||
'型号规格',
|
||||
'类别',
|
||||
'单位',
|
||||
'税率',
|
||||
'除税基价',
|
||||
'含税基价',
|
||||
'除税编制价',
|
||||
'含税编制价',
|
||||
'计算基数'
|
||||
]
|
||||
export const nameRenderer = (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
// 清空单元格内容
|
||||
td.innerHTML = ''
|
||||
// 检查是否需要设置验证背景色
|
||||
const cellMeta = instance.getCellMeta(row, col)
|
||||
const isValid = cellMeta?.valid !== false
|
||||
|
||||
// 如果单元格被标记为无效,设置红色背景
|
||||
if (!isValid) {
|
||||
td.style.backgroundColor = '#ffbeba' // 淡红色背景
|
||||
} else {
|
||||
td.style.backgroundColor = '' // 清除背景色
|
||||
}
|
||||
|
||||
|
||||
// 获取当前行数据
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
const isMerged = rowData?.isMerged
|
||||
|
||||
// 创建容器
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = 'display: flex; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 0 5px;'
|
||||
|
||||
// 创建文本显示
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value || ''
|
||||
textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;'
|
||||
|
||||
container.appendChild(textSpan)
|
||||
// 当 isMerged === 1 或 isMerged === true 时显示复合工料机图标
|
||||
if (isMerged === 1 || isMerged === true) {
|
||||
// 创建复合工料机图标(文件夹加号图标)
|
||||
const iconSpan = document.createElement('span')
|
||||
iconSpan.innerHTML = '<svg t="1766906584191" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7071" width="16" height="16"><path d="M905.874286 157.549714H417.499429l-70.948572-133.851428c0-15.798857-15.725714-23.698286-31.451428-23.698286H118.125714C55.149714 0 0 55.149714 0 118.125714v787.748572C0 968.850286 55.149714 1024 118.125714 1024h787.748572c62.976 0 118.125714-55.149714 118.125714-118.125714V275.675429c0-62.976-55.149714-118.125714-118.125714-118.125715z m39.350857 748.251429c0 23.698286-15.725714 39.497143-39.350857 39.497143H118.125714c-23.625143 0-39.350857-15.798857-39.350857-39.497143V118.198857c0-23.625143 15.725714-39.350857 39.350857-39.350857h173.348572l70.875428 133.851429c0 15.798857 15.725714 23.698286 31.451429 23.698285h512c23.698286 0 39.497143 15.725714 39.497143 39.350857v630.198858l-0.073143-0.146286z" fill="#2c48fc" p-id="7072"></path><path d="M475.428571 548.571429v-146.285715a36.571429 36.571429 0 0 1 73.142858 0v146.285715h146.285714a36.571429 36.571429 0 0 1 0 73.142857h-146.285714v146.285714a36.571429 36.571429 0 1 1-73.142858 0v-146.285714h-146.285714a36.571429 36.571429 0 0 1 0-73.142857h146.285714z" fill="#2c48fc" p-id="7073"></path></svg>'
|
||||
iconSpan.style.cssText = 'margin-left: 5px; display: inline-flex; align-items: center; flex-shrink: 0;'
|
||||
iconSpan.title = '复合工料机'
|
||||
|
||||
container.appendChild(iconSpan)
|
||||
}
|
||||
|
||||
td.appendChild(container)
|
||||
|
||||
return td
|
||||
}
|
||||
|
||||
export const calcRenderer = (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
// 清空单元格内容
|
||||
td.innerHTML = ''
|
||||
|
||||
// 获取当前行数据
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
const unit = rowData?.unit
|
||||
|
||||
// 只有当 unit === '%' 时才可编辑,否则设置为只读
|
||||
if (unit !== '%') {
|
||||
cellProperties.readOnly = true
|
||||
// 添加只读样式
|
||||
td.style.backgroundColor = '#f5f5f5'
|
||||
td.style.color = '#999'
|
||||
td.textContent = value || ''
|
||||
return td
|
||||
}
|
||||
|
||||
// 创建容器
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = 'display: flex; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 0 5px;'
|
||||
|
||||
// 创建文本显示
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value || ''
|
||||
textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;'
|
||||
|
||||
container.appendChild(textSpan)
|
||||
|
||||
// 只有当 unit == '%' 时才显示编辑图标
|
||||
if (unit === '%') {
|
||||
// 创建编辑图标
|
||||
const iconSpan = document.createElement('span')
|
||||
iconSpan.innerHTML = '<svg t="1766200593500" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8130" width="16" height="16"><path d="M251.2 387H320v68.8c0 1.8 1.8 3.2 4 3.2h48c2.2 0 4-1.4 4-3.3V387h68.8c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H376v-68.8c0-1.8-1.8-3.2-4-3.2h-48c-2.2 0-4 1.4-4 3.2V331h-68.8c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4z m328 0h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4z m0 265h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4z m0 104h193.6c1.8 0 3.2-1.8 3.2-4v-48c0-2.2-1.4-4-3.3-4H579.2c-1.8 0-3.2 1.8-3.2 4v48c0 2.2 1.4 4 3.2 4z m-195.7-81l61.2-74.9c4.3-5.2 0.7-13.1-5.9-13.1H388c-2.3 0-4.5 1-5.9 2.9l-34 41.6-34-41.6c-1.5-1.8-3.7-2.9-5.9-2.9h-50.9c-6.6 0-10.2 7.9-5.9 13.1l61.2 74.9-62.7 76.8c-4.4 5.2-0.8 13.1 5.8 13.1h50.8c2.3 0 4.5-1 5.9-2.9l35.5-43.5 35.5 43.5c1.5 1.8 3.7 2.9 5.9 2.9h50.8c6.6 0 10.2-7.9 5.9-13.1L383.5 675z" p-id="8131"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-36 732H180V180h664v664z" p-id="8132"></path></svg>'
|
||||
iconSpan.style.cssText = 'cursor: pointer; margin-left: 5px; display: inline-flex; align-items: center; color: black; pointer-events: auto;'
|
||||
iconSpan.title = '计算基数设置'
|
||||
// iconSpan.className = 'calc-icon-btn'
|
||||
|
||||
// 处理点击事件的函数
|
||||
const handleClick = (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
console.log('图标被点击了', { row, col, value, rowData })
|
||||
|
||||
// 调用 calcCallback 回调
|
||||
if (cellProperties.calcCallback && typeof cellProperties.calcCallback === 'function') {
|
||||
cellProperties.calcCallback(rowData, value, row, col, instance)
|
||||
}
|
||||
}
|
||||
|
||||
// 同时绑定 mousedown 和 click 事件
|
||||
iconSpan.addEventListener('mousedown', handleClick, true)
|
||||
// iconSpan.addEventListener('click', handleClick, true)//不知道为什么不生效
|
||||
|
||||
container.appendChild(iconSpan)
|
||||
}
|
||||
|
||||
td.appendChild(container)
|
||||
|
||||
return td
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed } from 'vue'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElSplitter,ElSplitterPanel,ElCard } from 'element-plus';
|
||||
import { DbCalc } from '#/components/db-calc'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import { DbTree } from '#/components/db-tree'
|
||||
import { Page } from '@vben/common-ui'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { topColHeaders } from './machine'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
// import { sourceDataObject } from '#/components/db-hst/mockData'
|
||||
import { ElCard, ElSplitter, ElSplitterPanel, ElTableV2 } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useBottomTable } from './composables/useBottomTable'
|
||||
import { useCalcDialog } from './composables/useCalcDialog'
|
||||
import { useCategoryTree } from './composables/useCategoryTree'
|
||||
import { useDetailTree } from './composables/useDetailTree'
|
||||
import { useItemSelect } from './composables/useItemSelect'
|
||||
import { useTopTable } from './composables/useTopTable'
|
||||
import { calcRenderer } from './machine'
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const { height: containerHeight } = useElementSize(containerRef)
|
||||
const topContainerRef = ref<HTMLElement | null>(null)
|
||||
@@ -15,409 +21,209 @@ const { height: topContainerHeight } = useElementSize(topContainerRef)
|
||||
const bottomContainerRef = ref<HTMLElement | null>(null)
|
||||
const { height: bottomContainerHeight } = useElementSize(bottomContainerRef)
|
||||
|
||||
type Tree = { id: string; label: string; children?: Tree[] }
|
||||
const categoryTreeData = ref<Tree[]>([
|
||||
{
|
||||
id: '1',
|
||||
label: '工料机总类',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
label: '广东',
|
||||
children: [
|
||||
{ id: '3', label: '广东工民建工料机' },
|
||||
{ id: '4', label: '广东公路工料机' },
|
||||
{ id: '5', label: '广东水利工料机' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '工料机总类2',
|
||||
children: [
|
||||
{
|
||||
id: '12',
|
||||
label: '广西',
|
||||
children: [
|
||||
{ id: '13', label: '工料机1' },
|
||||
{ id: '14', label: '工料机1' },
|
||||
{ id: '15', label: '工料机2' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const detailTreeData = ref<Tree[]>([
|
||||
{
|
||||
id: '1',
|
||||
label: '目录1',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
label: '黑色及有色金属',
|
||||
children: [
|
||||
{ id: '3', label: '钢筋' },
|
||||
{ id: '4', label: '钢丝' },
|
||||
{ id: '5', label: '钢纹线、网丝束' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '目录2',
|
||||
children: [
|
||||
{
|
||||
id: '12',
|
||||
label: '塑料、塑胶及非金属',
|
||||
children: [
|
||||
{ id: '13', label: '塑料' },
|
||||
{ id: '14', label: '塑胶' },
|
||||
{ id: '15', label: '非金属' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const topColumns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码', renderer: codeRenderer},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'spec',title:'型号规格'},
|
||||
{type:'text',data:'category',title:'类别'},
|
||||
{type:'text',data:'unit',title:'单位'},
|
||||
{type:'text',data:'taxRate',title:'税率'},
|
||||
{type:'text',data:'priceExTax',title:'除税基价'},
|
||||
{type:'text',data:'priceInTax',title:'含税基价'},
|
||||
{type:'text',data:'priceExTaxComp',title:'除税编制价'},
|
||||
{type:'text',data:'priceInTaxComp',title:'含税编制价'},
|
||||
{type:'text',data:'calcBase',title:'计算基数'}
|
||||
])
|
||||
const tableHeaders = ref<string[]>([
|
||||
'编码',
|
||||
'名称',
|
||||
'型号规格',
|
||||
'类别',
|
||||
'单位',
|
||||
'税率',
|
||||
'除税基价',
|
||||
'含税基价',
|
||||
'除税编制价',
|
||||
'含税编制价',
|
||||
'计算基数'
|
||||
])
|
||||
const mergedId = ref<number>()
|
||||
const detailTreeRef = ref<any>()
|
||||
|
||||
// 生成 tableData 模拟数据
|
||||
const generateTableData = () => {
|
||||
const categories = ['人', '人机', '材', '机']
|
||||
const units = ['台', '个', 't', 'm³', 'm²', 'kg']
|
||||
|
||||
return Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `TBL${String(index + 1).padStart(6, '0')}`,
|
||||
name: `表格项目${index + 1}`,
|
||||
spec: `规格${index + 1}`,
|
||||
category: categories[index % categories.length],
|
||||
unit: units[index % units.length],
|
||||
taxRate: `${(Math.random() * 0.13 + 0.03).toFixed(2)}`,
|
||||
priceExTax: (Math.random() * 8000 + 800).toFixed(2),
|
||||
priceInTax: (Math.random() * 9000 + 900).toFixed(2),
|
||||
priceExTaxComp: (Math.random() * 8500 + 850).toFixed(2),
|
||||
priceInTaxComp: (Math.random() * 9500 + 950).toFixed(2),
|
||||
calcBase: (Math.random() * 80 + 1).toFixed(2)
|
||||
}))
|
||||
}
|
||||
const {
|
||||
categoryTreeData,
|
||||
catalogsTreeId,
|
||||
loadCategoryTree,
|
||||
handleSelect: categoryHandleSelect
|
||||
} = useCategoryTree()
|
||||
|
||||
const tableData = ref<any[]>(generateTableData())
|
||||
const bottomColumns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'编码'},
|
||||
{type:'text',data:'name',title:'名称',
|
||||
renderer: 'db-table',
|
||||
customDropdownSource: tableData.value,
|
||||
customGetLabel: (item: any) => item?.name,
|
||||
customTableHeaders: tableHeaders.value,
|
||||
// 自定义字段键,按顺序对应表头
|
||||
customFieldKeys: ['code', 'name', 'spec', 'category', 'unit', 'taxRate', 'priceExTax', 'priceInTax', 'priceExTaxComp', 'priceInTaxComp', 'calcBase'],
|
||||
// 自定义搜索字段
|
||||
customSearchFields: ['code', 'name', 'spec', 'category', 'unit']
|
||||
},
|
||||
{type:'text',data:'spec',title:'型号规格'},
|
||||
{type:'text',data:'category',title:'类别'},
|
||||
{type:'text',data:'unit',title:'单位'},
|
||||
{type:'text',data:'taxRate',title:'税率'},
|
||||
{type:'text',data:'priceExTax',title:'除税基价'},
|
||||
const {
|
||||
detailTreeData,
|
||||
catalogItemId,
|
||||
catalogsTreeId: detailCatalogsTreeId,
|
||||
catalogueMenuHandler,
|
||||
loadDetailTree,
|
||||
handleSelect: detailHandleSelect,
|
||||
handleEditSave: handleCatalogItemEditSave
|
||||
} = useDetailTree(catalogsTreeId)
|
||||
|
||||
{type:'text',data:'priceExTax1',title:'除税编制价'},
|
||||
{type:'text',data:'priceExTax2',title:'含税编制价'},
|
||||
{type:'text',data:'priceExTax3',title:'定额消耗量'},
|
||||
{type:'text',data:'priceExTax4',title:'含税编制合价'},
|
||||
{type:'text',data:'priceExTax5',title:'除税基价合价'},
|
||||
{type:'text',data:'priceExTax6',title:'含税基价合价'},
|
||||
{type:'text',data:'priceExTax7',title:'除税编制合价'},
|
||||
{type:'text',data:'calcBase',title:'计算基数(隐藏)'},
|
||||
const {
|
||||
bottomHstRef,
|
||||
bottomDbSettings,
|
||||
bottomContextMenuItems,
|
||||
loadData: loadBottomData,
|
||||
updateHeight: updateBottomHeight,
|
||||
updateNameColumn
|
||||
} = useBottomTable(mergedId)
|
||||
|
||||
])
|
||||
// const colHeaders = ref<string[]>(topColHeaders)
|
||||
const topHstRef = ref<any>(null)
|
||||
const bottomHstRef = ref<any>(null)
|
||||
let rowSchema: any = {level: null,__children: []}
|
||||
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
topColumns.value.forEach((col: any) => {
|
||||
if (col.data && col.data !== 'level' && col.data !== '__children') {
|
||||
rowSchema[col.data] = null
|
||||
const {
|
||||
topHstRef,
|
||||
topColumns,
|
||||
topDbSettings,
|
||||
topContextMenuItems,
|
||||
calcTableData,
|
||||
loadData: loadTopData,
|
||||
loadAllowedCategories,
|
||||
loadTopDataClear
|
||||
} = useTopTable(catalogItemId, mergedId, loadBottomData)
|
||||
|
||||
const {
|
||||
calcDialogVisible,
|
||||
currentCalcValue,
|
||||
openDialog: openCalcDialog,
|
||||
handleConfirm: handleCalcConfirm
|
||||
} = useCalcDialog(topHstRef)
|
||||
|
||||
const {
|
||||
visible,
|
||||
buttonRef,
|
||||
popoverRef,
|
||||
popoverTableData,
|
||||
popoverColumns,
|
||||
selectRender,
|
||||
rowEventHandlers,
|
||||
handleTableEndReached,
|
||||
createCallbacks
|
||||
} = useItemSelect(mergedId, catalogItemId)
|
||||
|
||||
const itemCallbacks = createCallbacks()
|
||||
|
||||
topColumns.push({
|
||||
type: 'text',
|
||||
data: 'calcBase.formula',
|
||||
title: '计算基数',
|
||||
renderer: calcRenderer,
|
||||
calcCallback: (_currentRowData: any, _value: any, row: number) => {
|
||||
openCalcDialog(row)
|
||||
}
|
||||
})
|
||||
// console.log(rowSchema)
|
||||
|
||||
const topMock = ()=>{
|
||||
// 生成带层级结构的模拟数据
|
||||
const categories = ['人', '人机', '材', '机']
|
||||
const units = ['台', '个', 't', 'm³', 'm²', 'kg']
|
||||
|
||||
const mockData = []
|
||||
let codeCounter = 1
|
||||
|
||||
// 生成5个父级项目
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const parentCode = `MAC${String(codeCounter++).padStart(6, '0')}`
|
||||
const parent = {
|
||||
code: parentCode,
|
||||
name: `机械设备分类${i + 1}`,
|
||||
spec: `规格${i + 1}`,
|
||||
category: categories[i % categories.length],
|
||||
unit: units[i % units.length],
|
||||
taxRate: `${(Math.random() * 0.13 + 0.03).toFixed(2)}`,
|
||||
priceExTax: (Math.random() * 10000 + 1000).toFixed(2),
|
||||
priceInTax: (Math.random() * 11000 + 1100).toFixed(2),
|
||||
priceExTaxComp: (Math.random() * 10500 + 1050).toFixed(2),
|
||||
priceInTaxComp: (Math.random() * 11500 + 1150).toFixed(2),
|
||||
calcBase: (Math.random() * 100 + 1).toFixed(2),
|
||||
level: String(i),
|
||||
__children: []
|
||||
}
|
||||
updateNameColumn(selectRender, itemCallbacks)
|
||||
|
||||
const handleCategorySelect = async (node: any) => {
|
||||
console.log(node)
|
||||
await categoryHandleSelect(node)
|
||||
// 同步更新 detailTree 的 catalogsTreeId
|
||||
detailCatalogsTreeId.value = node.id
|
||||
await loadDetailTree(node.id)
|
||||
await loadTopDataClear()
|
||||
detailTreeRef?.value.setCurrentKey(null)
|
||||
}
|
||||
|
||||
const handleDetailSelect = async (node: any) => {
|
||||
const id = detailHandleSelect(node)
|
||||
if (id) {
|
||||
|
||||
// 为每个父级生成2-4个子项
|
||||
const childCount = Math.floor(Math.random() * 3) + 2
|
||||
for (let j = 0; j < childCount; j++) {
|
||||
const childCode = `${parentCode}-${String(j + 1).padStart(3, '0')}`
|
||||
const child = {
|
||||
code: childCode,
|
||||
name: `${parent.name}-子项${j + 1}`,
|
||||
spec: `规格${i + 1}-${j + 1}`,
|
||||
category: categories[(i + j) % categories.length],
|
||||
unit: units[(i + j) % units.length],
|
||||
taxRate: `${(Math.random() * 0.13 + 0.03).toFixed(2)}`,
|
||||
priceExTax: (Math.random() * 5000 + 500).toFixed(2),
|
||||
priceInTax: (Math.random() * 5500 + 550).toFixed(2),
|
||||
priceExTaxComp: (Math.random() * 5200 + 520).toFixed(2),
|
||||
priceInTaxComp: (Math.random() * 5700 + 570).toFixed(2),
|
||||
calcBase: (Math.random() * 50 + 1).toFixed(2),
|
||||
level: `${i}-${j + 1}`,
|
||||
__children: []
|
||||
}
|
||||
|
||||
// 为部分子项生成孙项
|
||||
if (j === 0 && i < 3) {
|
||||
const grandChildCount = Math.floor(Math.random() * 2) + 1
|
||||
for (let k = 0; k < grandChildCount; k++) {
|
||||
const grandChild = {
|
||||
code: `${childCode}-${String(k + 1).padStart(2, '0')}`,
|
||||
name: `${child.name}-明细${k + 1}`,
|
||||
spec: `规格${i + 1}-${j + 1}-${k + 1}`,
|
||||
category: categories[(i + j + k) % categories.length],
|
||||
unit: units[(i + j + k) % units.length],
|
||||
taxRate: `${(Math.random() * 0.13 + 0.03).toFixed(2)}`,
|
||||
priceExTax: (Math.random() * 2000 + 200).toFixed(2),
|
||||
priceInTax: (Math.random() * 2200 + 220).toFixed(2),
|
||||
priceExTaxComp: (Math.random() * 2100 + 210).toFixed(2),
|
||||
priceInTaxComp: (Math.random() * 2300 + 230).toFixed(2),
|
||||
calcBase: (Math.random() * 20 + 1).toFixed(2),
|
||||
level: `${i}-${j + 1}-${k + 1}`,
|
||||
__children: []
|
||||
}
|
||||
child.__children.push(grandChild)
|
||||
}
|
||||
}
|
||||
|
||||
parent.__children.push(child)
|
||||
}
|
||||
|
||||
mockData.push(parent)
|
||||
await loadAllowedCategories()
|
||||
await loadTopData(id)
|
||||
}
|
||||
|
||||
return mockData
|
||||
}
|
||||
|
||||
let topDbSettings = {
|
||||
data: topMock(),
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns: topColumns.value,
|
||||
// preventOverflow: 'horizontal',
|
||||
rowHeaders: false,
|
||||
nestedRows: true,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
separator1: '---------',
|
||||
custom_add_child: {
|
||||
name: '添加子行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'child')
|
||||
}
|
||||
},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'delete')
|
||||
}
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
const bootomMock = ()=>{
|
||||
// 生成模拟数据
|
||||
const categories = ['人', '人机', '材', '机']
|
||||
const units = ['台', '个', 't', 'm³', 'm²', 'kg']
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `DTL${String(index + 1).padStart(6, '0')}`,
|
||||
name: `明细项目${index + 1}`,
|
||||
spec: `规格${index + 1}`,
|
||||
category: categories[index % categories.length],
|
||||
unit: units[index % units.length],
|
||||
taxRate: `${(Math.random() * 0.13 + 0.03).toFixed(2)}`,
|
||||
priceExTax: (Math.random() * 5000 + 500).toFixed(2),
|
||||
priceExTax1: (Math.random() * 5200 + 520).toFixed(2),
|
||||
priceExTax2: (Math.random() * 5800 + 580).toFixed(2),
|
||||
priceExTax3: (Math.random() * 10 + 1).toFixed(2),
|
||||
priceExTax4: (Math.random() * 60000 + 6000).toFixed(2),
|
||||
priceExTax5: (Math.random() * 55000 + 5500).toFixed(2),
|
||||
priceExTax6: (Math.random() * 62000 + 6200).toFixed(2),
|
||||
priceExTax7: (Math.random() * 58000 + 5800).toFixed(2),
|
||||
calcBase: (Math.random() * 100 + 1).toFixed(2)
|
||||
}))
|
||||
return mockData;
|
||||
}
|
||||
let bottomDbSettings = {
|
||||
columns: bottomColumns.value,
|
||||
}
|
||||
const categoryHandleSelect = (node: Tree) => {
|
||||
console.log('categoryhandleSelect',node)
|
||||
}
|
||||
|
||||
const detailHandleSelect = (node: Tree) => {
|
||||
if (topHstRef.value && typeof topHstRef.value.loadData === 'function') {
|
||||
// console.log('hstData.value',hstData.value)
|
||||
// topHstRef.value.loadData(topHstData.value)
|
||||
}
|
||||
// // 访问内部的 hotTableComponent
|
||||
// if (dbHstRef.value?.hotTableComponent) {
|
||||
// console.log('hotTableComponent', dbHstRef.value.hotTableComponent)
|
||||
// }
|
||||
// // 访问 hotInstance
|
||||
// if (dbHstRef.value?.hotInstance) {
|
||||
// dbHstRef.value?.hotInstance.loadData(topHstData.value)
|
||||
// console.log('hotInstance', dbHstRef.value.hotInstance)
|
||||
// }
|
||||
}
|
||||
// watch(
|
||||
// () => topContainerHeight.value,
|
||||
// (val) => {
|
||||
// console.log('topContainerHeight', val);
|
||||
// },
|
||||
// { immediate: true },
|
||||
// )
|
||||
function onResizeEnd(index: number, sizes: number[]) {
|
||||
// 通过 hotInstance 来操作 Handsontable
|
||||
console.log(index,sizes)
|
||||
const onResizeEnd = (_index: number, sizes: number[]) => {
|
||||
onTopHeight(sizes[0])
|
||||
onBottomHeight(sizes[1])
|
||||
}
|
||||
function onTopHeight(height: number){
|
||||
|
||||
const onTopHeight = (height: number) => {
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.updateSettings({
|
||||
height: height,
|
||||
})
|
||||
|
||||
// topHstRef.value.loadData(topMock())
|
||||
|
||||
// 更新 code 列的宽度
|
||||
topHstRef.value.updateCodeColWidth()
|
||||
|
||||
console.log('onResizeEnd-onTopHeight',height, 'codeColWidth:', topHstRef.value.codeColWidth);
|
||||
}
|
||||
}
|
||||
function onBottomHeight(height: number){
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
bottomHstRef.value.hotInstance.updateSettings({
|
||||
height: height
|
||||
})
|
||||
|
||||
bottomHstRef.value.loadData(bootomMock())
|
||||
bottomHstRef.value.hotInstance.render()
|
||||
console.log('onResizeEnd-bottomHstRef',height);
|
||||
topHstRef.value.hotInstance.updateSettings({ height: height - 8 })
|
||||
}
|
||||
}
|
||||
|
||||
const onBottomHeight = (height: number) => {
|
||||
updateBottomHeight(height)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategoryTree()
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
onTopHeight(topContainerHeight.value)
|
||||
onBottomHeight(bottomContainerHeight.value)
|
||||
}, 200);
|
||||
}, 200)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel collapsible size="15%" :min="200">
|
||||
<Page auto-content-height>
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="100">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" ref="containerRef">
|
||||
<DbTree :height="containerHeight" :data="categoryTreeData" @select="categoryHandleSelect" :defaultExpandedKeys="2" :search="false" />
|
||||
<DbTree
|
||||
:height="containerHeight"
|
||||
:data="categoryTreeData"
|
||||
@select="handleCategorySelect"
|
||||
:defaultExpandedKeys="2"
|
||||
:search="false"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible size="15%" :min="200">
|
||||
|
||||
<ElSplitterPanel size="15%" :min="100">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||
<DbTree :height="containerHeight" :data="detailTreeData" @select="detailHandleSelect" :defaultExpandedKeys="2" :search="false" />
|
||||
<DbTree
|
||||
ref="detailTreeRef"
|
||||
:height="containerHeight"
|
||||
:data="detailTreeData"
|
||||
@select="handleDetailSelect"
|
||||
@edit-save="handleCatalogItemEditSave"
|
||||
:defaultExpandedKeys="2"
|
||||
:search="false"
|
||||
:context-menu-handler="catalogueMenuHandler"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel >
|
||||
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElSplitter layout="vertical" @resize-end="onResizeEnd">
|
||||
<ElSplitterPanel collapsible size="60%" :min="200">
|
||||
<ElSplitterPanel size="60%" :min="200">
|
||||
<ElCard class="w-full h-full" body-class="!p-0 h-full" ref="topContainerRef">
|
||||
<DbHst ref="topHstRef" :settings="topDbSettings"></DbHst>
|
||||
<DbHst ref="topHstRef" :settings="topDbSettings" :contextMenuItems="topContextMenuItems" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full" body-class="!p-0 h-full" ref="bottomContainerRef">
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings" :contextMenuItems="bottomContextMenuItems" />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
|
||||
<ElPopover
|
||||
ref="popoverRef"
|
||||
:virtual-ref="buttonRef"
|
||||
virtual-triggering
|
||||
:visible="visible"
|
||||
placement="bottom"
|
||||
:width="955"
|
||||
:popper-style="{ height: '380px' }"
|
||||
>
|
||||
<ElTableV2
|
||||
:columns="popoverColumns"
|
||||
:data="popoverTableData"
|
||||
:width="930"
|
||||
:height="350"
|
||||
fixed
|
||||
:row-height="25"
|
||||
:header-height="25"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
:cell-props="{ style: { background: 'transparent !important' } }"
|
||||
:teleported="false"
|
||||
@end-reached="handleTableEndReached"
|
||||
/>
|
||||
</ElPopover>
|
||||
|
||||
<DbCalc
|
||||
v-model="calcDialogVisible"
|
||||
:current-value="currentCalcValue"
|
||||
@confirm="handleCalcConfirm"
|
||||
:table-data="calcTableData"
|
||||
:table-column="[
|
||||
{ prop: 'id', label: '序号' },
|
||||
{ prop: 'name', label: '名称' },
|
||||
{ prop: 'code', label: '类型' }
|
||||
]"
|
||||
table-prop="code"
|
||||
mode="resource"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ watch(
|
||||
console.log('MarketMaterials newHeight', newHeight)
|
||||
if (newHeight && hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
})
|
||||
hstRef.value.hotInstance.render()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// import { ref } from 'vue'
|
||||
// import { getDictOptions } from '@vben/hooks';
|
||||
// import { DICT_TYPE } from '@vben/constants';
|
||||
// const settingRender = (
|
||||
// instance: any,
|
||||
// TD: HTMLTableCellElement,
|
||||
// row: number,
|
||||
// col: number,
|
||||
// prop: any,
|
||||
// value: any,
|
||||
// cellProperties: any
|
||||
// ) => {
|
||||
// TD.innerHTML = ''
|
||||
|
||||
// const container = document.createElement('div')
|
||||
// container.style.display = 'flex'
|
||||
// container.style.alignItems = 'center'
|
||||
// container.style.justifyContent = 'center'
|
||||
// container.style.cursor = 'pointer'
|
||||
// container.style.color = '#006be6'
|
||||
// container.style.padding = '0 5px'
|
||||
|
||||
// const textSpan = document.createElement('span')
|
||||
// textSpan.textContent = '编辑'
|
||||
|
||||
// container.addEventListener('click', (e) => {
|
||||
// e.stopPropagation()
|
||||
// })
|
||||
|
||||
// container.appendChild(textSpan)
|
||||
// TD.appendChild(container)
|
||||
|
||||
// return TD
|
||||
// }
|
||||
|
||||
// export const columns = ref<any[]>([
|
||||
// { type: 'text', data: 'sorderId', title: '序号' },
|
||||
// { type: 'text', data: 'name', title: '名称' },
|
||||
// { type: 'text', data: 'value', title: '定额值' },
|
||||
// { type: 'numeric', data: 'coefficient2', title: '调整' },
|
||||
// { type: 'numeric', data: 'coefficient1', title: '调整内容' },
|
||||
// { type: 'text', data: 'setting', title: '设置', renderer: settingRender, readOnly: true }
|
||||
// ])
|
||||
@@ -1,56 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import { getAdjustmentCombinedList } from '#/api/database/quota';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
quotaItemId?: number
|
||||
}>()
|
||||
|
||||
const topHstRef = ref<any>(null)
|
||||
const bottomHstRef = ref<any>(null)
|
||||
|
||||
// 第一个表格:调整、附注名称、附注内容
|
||||
// 第一个表格:序号、名称、调整
|
||||
const topColumns = ref<any[]>([
|
||||
{ type: 'checkbox', data: 'adjustment', title: '调整' },
|
||||
{ type: 'text', data: 'noteName', title: '附注名称' },
|
||||
{ type: 'text', data: 'noteContent', title: '附注内容' }
|
||||
{
|
||||
type: 'text',
|
||||
data: 'sortOrder',
|
||||
title: '序号',
|
||||
readOnly: true,
|
||||
width: 80,
|
||||
renderer: (_instance: any, td: HTMLTableCellElement, row: number) => {
|
||||
td.innerHTML = String(row + 1)
|
||||
td.style.textAlign = 'center'
|
||||
td.style.color = '#909399'
|
||||
return td
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'name', title: '名称', readOnly: true },
|
||||
{
|
||||
type: 'text',
|
||||
data: 'adjustment',
|
||||
title: '调整',
|
||||
className: 'htCenter',
|
||||
renderer: (instance: any, td: HTMLTableCellElement, row: number, _col: number, _prop: string, value: any, cellProperties: any) => {
|
||||
const rowData = instance.getSourceDataAtRow(row)
|
||||
const adjustmentType = rowData?.adjustmentType
|
||||
|
||||
// 清空单元格
|
||||
td.innerHTML = ''
|
||||
td.style.textAlign = 'center'
|
||||
td.style.verticalAlign = 'middle'
|
||||
|
||||
// 根据调整类型决定显示方式
|
||||
if (adjustmentType === 'dynamic_merge' || adjustmentType === 'dynamic_adjust') {
|
||||
// 录入框:使用默认的文本渲染(双击可编辑)
|
||||
cellProperties.readOnly = false
|
||||
td.innerHTML = value || ''
|
||||
return td
|
||||
} else {
|
||||
// 复选框:居中显示,禁止双击编辑
|
||||
cellProperties.readOnly = true
|
||||
const checkbox = document.createElement('input')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.checked = value === true || value === 1 || value === '1'
|
||||
checkbox.style.cssText = 'cursor: pointer;'
|
||||
|
||||
// 监听复选框变化
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
instance.setDataAtRowProp(row, 'adjustment', target.checked)
|
||||
})
|
||||
|
||||
// 阻止双击事件
|
||||
td.addEventListener('dblclick', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
td.appendChild(checkbox)
|
||||
return td
|
||||
}
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
const topMockData = () => {
|
||||
return Array.from({ length: 10 }, (_, index) => ({
|
||||
adjustment: index % 2 === 0, // checkbox 类型使用 boolean 值
|
||||
noteName: `附注${index + 1}`,
|
||||
noteContent: `附注内容说明${index + 1}`
|
||||
}))
|
||||
}
|
||||
|
||||
const topSettings = {
|
||||
data: topMockData(),
|
||||
const topSettings = ref({
|
||||
data: [],
|
||||
columns: topColumns.value,
|
||||
colWidths: 150,
|
||||
height: 135
|
||||
}
|
||||
height: 135,
|
||||
readOnly: false, // 改为可编辑,因为调整字段需要编辑
|
||||
})
|
||||
|
||||
// 第二个表格:名称、定额值、实际值
|
||||
const bottomColumns = ref<any[]>([
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'quotaValue', title: '定额值' },
|
||||
{ type: 'text', data: 'actualValue', title: '实际值' }
|
||||
])
|
||||
// 加载调整设置与明细的组合列表数据
|
||||
const loadAdjustmentDetails = async () => {
|
||||
if (!props.quotaItemId) {
|
||||
console.warn('quotaItemId is required')
|
||||
// 清空表格
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const bottomMockData = () => {
|
||||
return Array.from({ length: 10 }, (_, index) => ({
|
||||
name: `项目${index + 1}`,
|
||||
quotaValue: (Math.random() * 100 + 10).toFixed(2),
|
||||
actualValue: (Math.random() * 100 + 10).toFixed(2)
|
||||
}))
|
||||
}
|
||||
|
||||
const bottomSettings = {
|
||||
data: bottomMockData(),
|
||||
columns: bottomColumns.value,
|
||||
colWidths: 150,
|
||||
height: 135
|
||||
try {
|
||||
console.log('开始加载调整明细列表, quotaItemId:', props.quotaItemId)
|
||||
const response = await getAdjustmentCombinedList(props.quotaItemId)
|
||||
console.log('接口返回数据:', response)
|
||||
|
||||
// 组合列表返回的是调整设置数组,每个设置包含其明细
|
||||
// 需要将其展平为表格行数据
|
||||
const flattenedData: any[] = []
|
||||
|
||||
if (response && Array.isArray(response)) {
|
||||
response.forEach((setting: any) => {
|
||||
console.log('处理调整设置:', setting)
|
||||
|
||||
// 从 adjustmentRules 中提取 type
|
||||
const adjustmentType = setting.adjustmentRules?.type || setting.adjustmentType
|
||||
console.log('提取的 adjustmentType:', adjustmentType)
|
||||
|
||||
// 如果有明细,展开明细
|
||||
if (setting.details && Array.isArray(setting.details) && setting.details.length > 0) {
|
||||
setting.details.forEach((detail: any, index: number) => {
|
||||
flattenedData.push({
|
||||
sortOrder: detail.sortOrder || (index + 1),
|
||||
name: setting.name, // 使用调整设置的名称
|
||||
quotaValue: setting.quotaValue, // 使用调整设置的定额值
|
||||
adjustmentType: adjustmentType, // 使用从 adjustmentRules 提取的 type
|
||||
adjustmentCode: detail.adjustmentCode, // 明细的调整代码
|
||||
adjustment: detail.adjustment, // 明细的调整内容
|
||||
settingId: setting.id,
|
||||
detailId: detail.id
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 如果没有明细,只显示调整设置本身
|
||||
flattenedData.push({
|
||||
sortOrder: setting.sortOrder || 0,
|
||||
name: setting.name,
|
||||
quotaValue: setting.quotaValue,
|
||||
adjustmentType: adjustmentType, // 使用从 adjustmentRules 提取的 type
|
||||
adjustmentCode: '',
|
||||
adjustment: setting.adjustmentContent || '', // 使用调整设置的调整内容
|
||||
settingId: setting.id,
|
||||
detailId: null
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log('展平后的数据:', flattenedData)
|
||||
|
||||
// 更新表格数据
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.loadData(flattenedData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load adjustment combined list:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -58,41 +154,50 @@ watch(
|
||||
(newHeight) => {
|
||||
console.log('QuotaAdjustment newHeight', newHeight)
|
||||
if (newHeight) {
|
||||
const tableHeight = (newHeight - 50 - 15) / 2 // 平均分配给两个表格
|
||||
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.updateSettings({
|
||||
height: tableHeight
|
||||
})
|
||||
topHstRef.value.hotInstance.render()
|
||||
}
|
||||
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
bottomHstRef.value.hotInstance.updateSettings({
|
||||
height: tableHeight
|
||||
})
|
||||
bottomHstRef.value.hotInstance.render()
|
||||
}
|
||||
const tableHeight = newHeight - 50
|
||||
nextTick(() => {
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.updateSettings({
|
||||
height: tableHeight
|
||||
})
|
||||
topHstRef.value.hotInstance.render()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.quotaItemId,
|
||||
(newQuotaItemId) => {
|
||||
console.log('QuotaAdjustment: quotaItemId changed to', newQuotaItemId)
|
||||
if (newQuotaItemId) {
|
||||
loadAdjustmentDetails()
|
||||
} else {
|
||||
// 清空表格
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
}
|
||||
}
|
||||
// 移除 immediate: true,避免与 onMounted 重复调用
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
console.log('QuotaAdjustment mounted')
|
||||
console.log('QuotaAdjustment mounted, quotaItemId:', props.quotaItemId)
|
||||
// onMounted 时调用一次
|
||||
if (props.quotaItemId) {
|
||||
loadAdjustmentDetails()
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ topHstRef, bottomHstRef })
|
||||
defineExpose({ topHstRef, loadAdjustmentDetails })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col gap-2">
|
||||
<div class="flex-1">
|
||||
<DbHst ref="topHstRef" :settings="topSettings" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<DbHst ref="bottomHstRef" :settings="bottomSettings" />
|
||||
</div>
|
||||
<DbHst ref="topHstRef" :settings="topSettings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { ref } from 'vue';
|
||||
//调整人材机消耗量 表头:名称,类别,系数
|
||||
export const tab1Columns = ref<any[]>([
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{
|
||||
type: 'text',
|
||||
data: 'category',
|
||||
title: '类别',
|
||||
renderer: 'db-dropdown',
|
||||
source: [
|
||||
{ label: '人工', value: '人工' },
|
||||
{ label: '材料', value: '材料' },
|
||||
{ label: '机械', value: '机械' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'numeric',
|
||||
data: 'coefficient',
|
||||
title: '系数',
|
||||
numericFormat: {
|
||||
pattern: '0,0.000',
|
||||
culture: 'en-US'
|
||||
},
|
||||
validator: (value: any, callback: (valid: boolean) => void) => {
|
||||
// 允许空值、数字(包括负数和小数)
|
||||
if (value === null || value === undefined || value === '') {
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
// 验证是否为有效数字(支持负数和小数)
|
||||
const numValue = Number(value)
|
||||
callback(!isNaN(numValue))
|
||||
}
|
||||
},
|
||||
])
|
||||
//动态合并定额 表头:序号,编码,名称,最小值,最大值,分母值,基础值,取值规则
|
||||
export const tab2Columns = ref<any[]>([
|
||||
{
|
||||
type: 'text',
|
||||
data: 'index',
|
||||
title: '序号',
|
||||
readOnly: true,
|
||||
renderer: (instance: any, td: HTMLTableCellElement, row: number) => {
|
||||
td.innerHTML = String(row + 1)
|
||||
td.style.textAlign = 'center'
|
||||
td.style.color = '#909399'
|
||||
return td
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'numeric', data: 'minValue', title: '最小值' },
|
||||
{ type: 'numeric', data: 'maxValue', title: '最大值' },
|
||||
{ type: 'numeric', data: 'denominatorValue', title: '分母值' },
|
||||
{ type: 'numeric', data: 'baseValue', title: '基础值' },
|
||||
{ type: 'text', data: 'valueRule', title: '取值规则',
|
||||
renderer: 'db-dropdown',
|
||||
source: getDictOptions(DICT_TYPE.DYNAMIC_MERGE_RULE, 'string')
|
||||
},
|
||||
])
|
||||
|
||||
//增减材料消耗量 表头:序号,编码,名称,增减定额消耗量数值
|
||||
export const tab3Columns = ref<any[]>([
|
||||
{
|
||||
type: 'text',
|
||||
data: 'index',
|
||||
title: '序号',
|
||||
readOnly: true,
|
||||
renderer: (instance: any, td: HTMLTableCellElement, row: number) => {
|
||||
td.innerHTML = String(row + 1)
|
||||
td.style.textAlign = 'center'
|
||||
td.style.color = '#909399'
|
||||
return td
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{
|
||||
type: 'numeric',
|
||||
data: 'consumptionValue',
|
||||
title: '增减定额消耗量数值',
|
||||
numericFormat: {
|
||||
pattern: '0,0.000',
|
||||
culture: 'en-US'
|
||||
},
|
||||
validator: (value: any, callback: (valid: boolean) => void) => {
|
||||
// 允许空值、数字(包括负数和小数)
|
||||
if (value === null || value === undefined || value === '') {
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
// 验证是否为有效数字(支持负数和小数)
|
||||
const numValue = Number(value)
|
||||
callback(!isNaN(numValue))
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
//动态调整人材机消耗量 表头:类别,系数设置,最小值,最大值。分母值,基础值,取值规则
|
||||
export const tab4Columns = ref<any[]>([
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'numeric', data: 'coefficientSetting', title: '系数设置' },
|
||||
{ type: 'numeric', data: 'minValue', title: '最小值' },
|
||||
{ type: 'numeric', data: 'maxValue', title: '最大值' },
|
||||
{ type: 'numeric', data: 'denominatorValue', title: '分母值' },
|
||||
{ type: 'numeric', data: 'baseValue', title: '基础值' },
|
||||
{ type: 'text', data: 'valueRule', title: '取值规则' ,
|
||||
renderer: 'db-dropdown',
|
||||
source: getDictOptions(DICT_TYPE.DYNAMIC_TALENT_MATERIAL_MACHINE_RULE, 'string')
|
||||
},
|
||||
])
|
||||
@@ -1,148 +1,534 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { ElDialog,ElTabPane, ElTable ,ElTableColumn,ElInput,ElButton} from 'element-plus';
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import { createAdjustmentSetting, deleteAdjustmentSetting, getAdjustmentSettingList, updateAdjustmentSetting } from '#/api/database/quota/index';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog, ElMessage } from 'element-plus';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { tab1Columns, tab2Columns, tab3Columns, tab4Columns } from './QuotaAdjustmentSetting';
|
||||
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
quotaItemId?: number | null
|
||||
}>()
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
const topHstRef = ref<any>(null)
|
||||
const adjustmentSettingsData = ref<any[]>([])
|
||||
|
||||
// 加载调整设置列表
|
||||
const loadAdjustmentSettings = async (quotaItemId: number) => {
|
||||
try {
|
||||
console.log('开始加载调整设置列表, quotaItemId:', quotaItemId)
|
||||
|
||||
const res = await getAdjustmentSettingList(quotaItemId)
|
||||
console.log('调整设置列表原始数据:', res)
|
||||
|
||||
if (res && Array.isArray(res)) {
|
||||
adjustmentSettingsData.value = res.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || '',
|
||||
adjustType: item.adjustmentType || '',
|
||||
adjustRule: item.adjustmentRules ? JSON.stringify(item.adjustmentRules) : ''
|
||||
}))
|
||||
|
||||
console.log('调整设置列表转换后数据:', adjustmentSettingsData.value)
|
||||
|
||||
// 更新表格数据
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.loadData(adjustmentSettingsData.value)
|
||||
}
|
||||
} else {
|
||||
console.warn('调整设置列表数据为空或格式不正确')
|
||||
adjustmentSettingsData.value = []
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载调整设置列表失败:', error)
|
||||
adjustmentSettingsData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
const dialogVisible = ref(false)
|
||||
const activeTab = ref('tab1')
|
||||
const activeTab = ref('tab1Settings')
|
||||
const currentCell = reactive({ row: 0, col: 0 })
|
||||
// Tab 标签映射
|
||||
const tabLabels: Record<string, string> = {
|
||||
'tab1': '调整工料机系数',
|
||||
'tab2': '动态合并定额',
|
||||
'tab3': '增减耗量',
|
||||
'tab4': '动态调整人材机消耗量'
|
||||
'tab1Columns': '调整人材机消耗量',
|
||||
'tab2Columns': '动态合并定额',
|
||||
'tab3Columns': '增减材料消耗量',
|
||||
'tab4Columns': '动态调整人材机消耗量'
|
||||
}
|
||||
const typeLists: any = [
|
||||
{label:'调整人材机消耗量',value: 1,tab:'tab1Columns'},
|
||||
{label:'动态合并定额',value: 2,tab:'tab2Columns'},
|
||||
{label:'增减材料消耗量',value: 3,tab:'tab3Columns'},
|
||||
{label:'动态调整人材机消耗量',value: 4,tab:'tab4Columns'}
|
||||
]
|
||||
|
||||
// 列配置映射
|
||||
const tabColumnsMap: Record<string, any> = {
|
||||
'tab1Columns': tab1Columns,
|
||||
'tab2Columns': tab2Columns,
|
||||
'tab3Columns': tab3Columns,
|
||||
'tab4Columns': tab4Columns
|
||||
}
|
||||
|
||||
// Tab1: 调整工料机系数
|
||||
const coefficientData = ref([
|
||||
{ category: '人工', coefficient: 2, selected: false },
|
||||
{ category: '材料', coefficient: 1.5, selected: false },
|
||||
{ category: '机具', coefficient: 1.5, selected: false },
|
||||
{ category: '其他', coefficient: 2, selected: false }
|
||||
])
|
||||
const dbSettings = ref<any>({
|
||||
data: [],
|
||||
columns: [],
|
||||
colWidths: 150,
|
||||
height: 270
|
||||
})
|
||||
|
||||
// Tab2: 动态合并定额
|
||||
const quotaMergeData = ref([
|
||||
{ code: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' }
|
||||
])
|
||||
|
||||
// Tab3: 增减耗量
|
||||
const consumptionData = ref([
|
||||
{ code: '', name: '', value: '' }
|
||||
])
|
||||
|
||||
// Tab4: 动态调整人材机消耗量
|
||||
const dynamicAdjustData = ref([
|
||||
{ name: '', category: '', coefficient: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' }
|
||||
])
|
||||
|
||||
const tabDialog = ( hot: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: any,
|
||||
value: any,
|
||||
cellProperties: any)=>{
|
||||
TD.innerHTML = ''
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.justifyContent = 'space-between'
|
||||
container.style.padding = '0 5px'
|
||||
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value || ''
|
||||
|
||||
const iconSpan = document.createElement('span')
|
||||
iconSpan.innerHTML = '▼'
|
||||
iconSpan.style.cursor = 'pointer'
|
||||
iconSpan.style.marginLeft = '5px'
|
||||
iconSpan.style.fontSize = '12px'
|
||||
iconSpan.style.color = '#909399'
|
||||
|
||||
iconSpan.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
currentCell.row = row
|
||||
currentCell.col = col
|
||||
dialogVisible.value = true
|
||||
})
|
||||
|
||||
container.appendChild(textSpan)
|
||||
container.appendChild(iconSpan)
|
||||
TD.appendChild(container)
|
||||
|
||||
return TD;
|
||||
}
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (hstRef?.value.hotInstance) {
|
||||
const tabLabel = tabLabels[activeTab.value]
|
||||
hstRef?.value.hotInstance.setDataAtCell(currentCell.row, currentCell.col, tabLabel)
|
||||
if (!topHstRef.value?.hotInstance || !hstRef.value?.hotInstance) {
|
||||
ElMessage.error('表格实例未初始化')
|
||||
return
|
||||
}
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const addRow = (tabName: string) => {
|
||||
switch(tabName) {
|
||||
case 'tab2':
|
||||
quotaMergeData.value.push({ code: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' })
|
||||
break
|
||||
case 'tab3':
|
||||
consumptionData.value.push({ code: '', name: '', value: '' })
|
||||
break
|
||||
case 'tab4':
|
||||
dynamicAdjustData.value.push({ name: '', category: '', coefficient: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' })
|
||||
break
|
||||
try {
|
||||
// 获取弹窗内表格的数据
|
||||
const dialogData = topHstRef.value.hotInstance.getData()
|
||||
|
||||
// 过滤掉空行
|
||||
const validData = dialogData.filter((row: any) => {
|
||||
return row && row.some((cell: any) => cell !== null && cell !== '')
|
||||
})
|
||||
|
||||
if (validData.length === 0) {
|
||||
ElMessage.warning('请至少填写一行数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 根据当前 tab 构建对应的 JSON 结构
|
||||
let adjustmentRules: any = {}
|
||||
|
||||
switch(activeTab.value) {
|
||||
case 'tab1Columns': // 调整人材机消耗量
|
||||
adjustmentRules = {
|
||||
type: 'adjust_coefficient',
|
||||
items: validData.map((row: any) => ({
|
||||
name: row[0] || '',
|
||||
category: row[1] || '',
|
||||
coefficient: row[2] || 0
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab2Columns': // 动态合并定额
|
||||
adjustmentRules = {
|
||||
type: 'dynamic_merge',
|
||||
items: validData.map((row: any) => ({
|
||||
// 不保存序号,序号是自动生成的
|
||||
code: row[1] || '',
|
||||
name: row[2] || '',
|
||||
minValue: row[3] || 0,
|
||||
maxValue: row[4] || 0,
|
||||
denominatorValue: row[5] || 0,
|
||||
baseValue: row[6] || 0,
|
||||
valueRule: row[7] || ''
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab3Columns': // 增减材料消耗量
|
||||
adjustmentRules = {
|
||||
type: 'consumption_adjustment',
|
||||
items: validData.map((row: any) => ({
|
||||
// 不保存序号,序号是自动生成的
|
||||
code: row[1] || '',
|
||||
name: row[2] || '',
|
||||
consumptionValue: row[3] || 0
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab4Columns': // 动态调整人材机消耗量
|
||||
adjustmentRules = {
|
||||
type: 'dynamic_adjust',
|
||||
items: validData.map((row: any) => ({
|
||||
category: row[0] || '',
|
||||
coefficientSetting: row[1] || 0,
|
||||
minValue: row[2] || 0,
|
||||
maxValue: row[3] || 0,
|
||||
denominatorValue: row[4] || 0,
|
||||
baseValue: row[5] || 0,
|
||||
valueRule: row[6] || ''
|
||||
}))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 将 JSON 对象转换为字符串
|
||||
const adjustRuleStr = JSON.stringify(adjustmentRules)
|
||||
|
||||
// 更新主表当前行的 adjustRule 字段
|
||||
hstRef.value.hotInstance.setDataAtRowProp(currentCell.row, 'adjustRule', adjustRuleStr)
|
||||
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false
|
||||
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const settingRender = (
|
||||
instance: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
_prop: any,
|
||||
_value: any,
|
||||
_cellProperties: any
|
||||
) => {
|
||||
TD.innerHTML = ''
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.justifyContent = 'center'
|
||||
container.style.cursor = 'pointer'
|
||||
container.style.color = '#006be6'
|
||||
container.style.padding = '0 5px'
|
||||
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = '编辑'
|
||||
|
||||
container.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const rowObj = instance.getSourceDataAtRow(row)
|
||||
if(rowObj.adjustType) {
|
||||
// 根据 adjustType 找到对应的 tab
|
||||
const typeItem = typeLists.find((item: any) => item.value == rowObj.adjustType)
|
||||
if(typeItem && typeItem.tab) {
|
||||
activeTab.value = typeItem.tab
|
||||
// 更新 dbSettings 的列配置
|
||||
updateDbSettings(typeItem.tab)
|
||||
|
||||
// 加载已有的调整规则数据
|
||||
loadAdjustmentRulesData(rowObj.adjustRule, typeItem.tab)
|
||||
}
|
||||
currentCell.row = row
|
||||
currentCell.col = col
|
||||
dialogVisible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
container.appendChild(textSpan)
|
||||
TD.appendChild(container)
|
||||
|
||||
return TD
|
||||
}
|
||||
|
||||
// 加载调整规则数据到弹窗表格
|
||||
const loadAdjustmentRulesData = (adjustRuleStr: string, tabName: string) => {
|
||||
try {
|
||||
if (!adjustRuleStr) {
|
||||
// 如果没有数据,加载空数组
|
||||
dbSettings.value.data = []
|
||||
return
|
||||
}
|
||||
|
||||
const adjustmentRules = JSON.parse(adjustRuleStr)
|
||||
let tableData: any[] = []
|
||||
|
||||
switch(tabName) {
|
||||
case 'tab1Columns': // 调整人材机消耗量
|
||||
if (adjustmentRules.items && Array.isArray(adjustmentRules.items)) {
|
||||
tableData = adjustmentRules.items.map((item: any) => ({
|
||||
name: item.name || '',
|
||||
category: item.category || '',
|
||||
coefficient: item.coefficient || 0
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab2Columns': // 动态合并定额
|
||||
if (adjustmentRules.items && Array.isArray(adjustmentRules.items)) {
|
||||
tableData = adjustmentRules.items.map((item: any) => ({
|
||||
// 序号不需要加载,会自动生成
|
||||
code: item.code || '',
|
||||
name: item.name || '',
|
||||
minValue: item.minValue || 0,
|
||||
maxValue: item.maxValue || 0,
|
||||
denominatorValue: item.denominatorValue || 0,
|
||||
baseValue: item.baseValue || 0,
|
||||
valueRule: item.valueRule || ''
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab3Columns': // 增减材料消耗量
|
||||
if (adjustmentRules.items && Array.isArray(adjustmentRules.items)) {
|
||||
tableData = adjustmentRules.items.map((item: any) => ({
|
||||
// 序号不需要加载,会自动生成
|
||||
code: item.code || '',
|
||||
name: item.name || '',
|
||||
consumptionValue: item.consumptionValue || 0
|
||||
}))
|
||||
}
|
||||
break
|
||||
|
||||
case 'tab4Columns': // 动态调整人材机消耗量
|
||||
if (adjustmentRules.items && Array.isArray(adjustmentRules.items)) {
|
||||
tableData = adjustmentRules.items.map((item: any) => ({
|
||||
category: item.category || '',
|
||||
coefficientSetting: item.coefficientSetting || 0,
|
||||
minValue: item.minValue || 0,
|
||||
maxValue: item.maxValue || 0,
|
||||
denominatorValue: item.denominatorValue || 0,
|
||||
baseValue: item.baseValue || 0,
|
||||
valueRule: item.valueRule || ''
|
||||
}))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
dbSettings.value.data = tableData
|
||||
|
||||
// 等待 DOM 更新后加载数据
|
||||
nextTick(() => {
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
topHstRef.value.hotInstance.loadData(tableData)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载调整规则数据失败:', error)
|
||||
dbSettings.value.data = []
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 dbSettings 配置
|
||||
const updateDbSettings = (tabName: string) => {
|
||||
const columns = tabColumnsMap[tabName]
|
||||
if (columns) {
|
||||
dbSettings.value = {
|
||||
data: [],
|
||||
columns: columns.value,
|
||||
colWidths: 150,
|
||||
height: 270
|
||||
}
|
||||
// 等待 DOM 更新后重新渲染表格
|
||||
nextTick(() => {
|
||||
if (topHstRef.value?.hotInstance) {
|
||||
// topHstRef.value.hotInstance.updateSettings({
|
||||
// columns : columns.value,
|
||||
// data: []
|
||||
// })
|
||||
// topHstRef.value.hotInstance.loadData([])
|
||||
topHstRef.value.hotInstance.render()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
type: 'text',
|
||||
data: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
readOnly: true,
|
||||
renderer: (_instance: any, td: HTMLTableCellElement, row: number) => {
|
||||
td.innerHTML = String(row + 1)
|
||||
td.style.textAlign = 'center'
|
||||
td.style.color = '#909399'
|
||||
return td
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'quotaValue', title: '定额值' },
|
||||
{ type: 'text', data: 'adjustContent', title: '调整内容' },
|
||||
{ type: 'text', data: 'adjustType', title: '调整类型', renderer: tabDialog, width: 200},
|
||||
{ type: 'text', data: 'adjustRule', title: '调整规则' }
|
||||
{ type: 'text', data: 'adjustType', title: '调整类型', renderer: 'db-dropdown', width: 200,
|
||||
source: typeLists,
|
||||
onAfterSelect: (_instance: any, row: number, column: number, oldValue: any, newValue: string, _optData?: any) => {
|
||||
console.log('选择后回调:', { row, column, oldValue, newValue })
|
||||
if(newValue == oldValue){
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'adjustRule', title: '设置', renderer: settingRender, readOnly: true }
|
||||
])
|
||||
|
||||
const mockData = () => {
|
||||
const types = ['人工调整', '材料调整', '机械调整', '综合调整']
|
||||
const rules = ['系数调整', '价格调整', '数量调整', '比例调整']
|
||||
return Array.from({ length: 10 }, (_, index) => ({
|
||||
name: `调整项${index + 1}`,
|
||||
quotaValue: (Math.random() * 100 + 10).toFixed(2),
|
||||
adjustContent: `调整内容说明${index + 1}`,
|
||||
adjustType: '',
|
||||
adjustRule: rules[index % rules.length]
|
||||
}))
|
||||
}
|
||||
|
||||
const settings = {
|
||||
data: mockData(),
|
||||
data: [], // 初始为空,等待从接口加载
|
||||
columns: columns.value,
|
||||
colWidths: 120,
|
||||
height: 270
|
||||
height: 170,
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function(_key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
this.alter('insert_row_above', selectedRow, 1)
|
||||
},
|
||||
},
|
||||
row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function(_key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
this.alter('insert_row_below', selectedRow, 1)
|
||||
},
|
||||
},
|
||||
remove_row: {
|
||||
name: '移除行',
|
||||
callback: function(_key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
// 调用删除 API
|
||||
deleteAdjustmentSetting(rowData.id).then(res => {
|
||||
console.log('删除调整设置成功', res)
|
||||
ElMessage.success('删除成功')
|
||||
}).catch(err => {
|
||||
console.error('删除调整设置失败:', err)
|
||||
ElMessage.error('删除失败')
|
||||
})
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
afterChange(changes: any, source: string) {
|
||||
console.log('afterChange', changes, source)
|
||||
|
||||
// 忽略特定来源的变更
|
||||
if (!changes || source === 'loadData' || source === 'updateId') {
|
||||
return
|
||||
}
|
||||
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// 检查必填字段是否都已填写
|
||||
if (!rowData.name || !rowData.adjustType) {
|
||||
console.log('必填字段未完整,跳过保存')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否选择了定额子目
|
||||
if (!props.quotaItemId) {
|
||||
ElMessage.error('请先选择定额子目')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建保存数据
|
||||
const saveData: any = {
|
||||
quotaItemId: props.quotaItemId,
|
||||
name: rowData.name || '',
|
||||
adjustmentType: rowData.adjustType || '',
|
||||
adjustmentRules: rowData.adjustRule ? JSON.parse(rowData.adjustRule) : {}
|
||||
}
|
||||
|
||||
if (rowData.id == null) {
|
||||
// 调用创建接口
|
||||
console.log('afterChange-create', saveData)
|
||||
createAdjustmentSetting(saveData).then(res => {
|
||||
console.log('createAdjustmentSetting', res)
|
||||
// 更新当前行ID
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
ElMessage.error('新增失败')
|
||||
})
|
||||
} else {
|
||||
// 调用更新接口
|
||||
saveData.id = rowData.id
|
||||
console.log('afterChange-update', saveData)
|
||||
updateAdjustmentSetting(saveData).then(res => {
|
||||
console.log('updateAdjustmentSetting', res)
|
||||
ElMessage.success('更新成功')
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
ElMessage.error('更新失败')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// 计算属性:根据是否选择了定额子目来决定是否显示右键菜单
|
||||
const contextMenuItems = computed(() => {
|
||||
// 如果没有选择定额子目,返回空数组(不显示右键菜单)
|
||||
if (!props.quotaItemId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 如果选择了定额子目,返回完整的右键菜单
|
||||
return [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
//TODO 指定的tab
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
const dialogContextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
//TODO 指定的tab
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 监听 quotaItemId 变化
|
||||
watch(
|
||||
() => props.quotaItemId,
|
||||
(newQuotaItemId) => {
|
||||
console.log('QuotaAdjustmentSetting: quotaItemId 变化:', newQuotaItemId)
|
||||
if (newQuotaItemId) {
|
||||
loadAdjustmentSettings(newQuotaItemId)
|
||||
} else {
|
||||
// 清空数据
|
||||
adjustmentSettingsData.value = []
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.height,
|
||||
(newHeight) => {
|
||||
console.log('QuotaAdjustmentQuery newHeight', newHeight)
|
||||
nextTick(() => {
|
||||
if (newHeight && hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
})
|
||||
hstRef.value.hotInstance.render()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -154,18 +540,20 @@ defineExpose({ hstRef })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
<div class="w-full h-full" @contextmenu.prevent>
|
||||
<DbHst ref="hstRef" :settings="settings" :contextMenuItems="contextMenuItems"/>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="调数类型"
|
||||
width="50%"
|
||||
:title="tabLabels[activeTab] || '调整设置'"
|
||||
width="60%"
|
||||
:before-close="handleClose"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<ElTabs v-model="activeTab">
|
||||
<!-- Tab1: 调整工料机系数 -->
|
||||
<ElTabPane label="调整工料机系数" name="tab1">
|
||||
<DbHst v-if="dialogVisible" ref="topHstRef" :settings="dbSettings" :contextMenuItems="dialogContextMenuItems"></DbHst>
|
||||
<!-- <ElTabs v-model="activeTab" :tab-position="'top'">
|
||||
|
||||
<ElTabPane v-if="activeTab === 'tab1'" label="调整工料机系数" name="tab1">
|
||||
<ElTable :data="coefficientData" border style="width: 50%">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn prop="category" label="类别" width="200" />
|
||||
@@ -177,8 +565,7 @@ defineExpose({ hstRef })
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab2: 动态合并定额 -->
|
||||
<ElTabPane label="动态合并定额" name="tab2">
|
||||
<ElTabPane v-if="activeTab === 'tab2'" label="动态合并定额" name="tab2">
|
||||
<ElTable :data="quotaMergeData" border style="width: 100%">
|
||||
<ElTableColumn prop="code" label="编码" width="120">
|
||||
<template #default="scope">
|
||||
@@ -214,8 +601,8 @@ defineExpose({ hstRef })
|
||||
<ElButton @click="addRow('tab2')" style="margin-top: 10px">添加行</ElButton>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab3: 增减耗量 -->
|
||||
<ElTabPane label="增减耗量" name="tab3">
|
||||
|
||||
<ElTabPane v-if="activeTab === 'tab3'" label="增减耗量" name="tab3">
|
||||
<ElTable :data="consumptionData" border style="width: 100%">
|
||||
<ElTableColumn prop="code" label="编码" width="200">
|
||||
<template #default="scope">
|
||||
@@ -236,8 +623,8 @@ defineExpose({ hstRef })
|
||||
<ElButton @click="addRow('tab3')" style="margin-top: 10px">添加行</ElButton>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab4: 动态调整人材机消耗量 -->
|
||||
<ElTabPane label="动态调整人材机消耗量" name="tab4">
|
||||
|
||||
<ElTabPane v-if="activeTab === 'tab4'" label="动态调整人材机消耗量" name="tab4">
|
||||
<ElTable :data="dynamicAdjustData" border style="width: 100%">
|
||||
<ElTableColumn prop="name" label="名称" width="120">
|
||||
<template #default="scope">
|
||||
@@ -286,7 +673,7 @@ defineExpose({ hstRef })
|
||||
</ElTable>
|
||||
<ElButton @click="addRow('tab4')" style="margin-top: 10px">添加行</ElButton>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElTabs> -->
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -296,7 +683,7 @@ defineExpose({ hstRef })
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,53 +1,508 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { createQuotaResource, deleteQuotaResource, getAvailableResourceList, getQuotaResourceList, updateQuotaResource } from '#/api/database/quota/index'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||
import { createPopoverCellRenderer, usePopoverClickOutside } from '#/components/db-hst/popover'
|
||||
import DbHstPopover from '#/components/db-hst/popover.vue'
|
||||
import { DICT_TYPE } from '@vben/constants'
|
||||
import { getDictOptions } from '@vben/hooks'
|
||||
import type { RowEventHandlers } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
quotaItemId?: number | null
|
||||
}>()
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
//const quotaResourcesData = ref<any[]>([])
|
||||
const visible = ref(false)
|
||||
const buttonRef = ref<HTMLElement | null>()
|
||||
const popoverComponentRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'spec', title: '型号规格' },
|
||||
{ type: 'text', data: 'unit', title: '单位' },
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'text', data: 'taxRate', title: '税率' },
|
||||
{ type: 'text', data: 'basePriceExTax', title: '除税基价' },
|
||||
{ type: 'text', data: 'basePriceInTax', title: '含税基价' },
|
||||
{ type: 'text', data: 'compilePriceExTax', title: '除税编制价' },
|
||||
{ type: 'text', data: 'compilePriceInTax', title: '含税编制价' },
|
||||
{ type: 'text', data: 'quotaQuantity', title: '定额消耗量' },
|
||||
{ type: 'text', data: 'adjustQuantity', title: '调整消耗量' }
|
||||
const onClickOutside = () => {
|
||||
popoverComponentRef.value?.popoverRef?.hide()
|
||||
}
|
||||
// 使用 popover 点击外部关闭的 Hook
|
||||
usePopoverClickOutside(visible, onClickOutside)
|
||||
|
||||
// 分页状态管理
|
||||
const popoverPagination = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
loading: false,
|
||||
hasMore: true
|
||||
})
|
||||
|
||||
const getTableDataList = async (_query?: any, append: boolean = false) => {
|
||||
if (popoverPagination.loading || !props.quotaItemId) return
|
||||
|
||||
popoverPagination.loading = true
|
||||
try {
|
||||
// 构建查询参数
|
||||
const queryParams: { code?: string; name?: string; spec?: string } = {}
|
||||
if (_query?.code) queryParams.code = _query.code
|
||||
if (_query?.name) queryParams.name = _query.name
|
||||
if (_query?.spec) queryParams.spec = _query.spec
|
||||
|
||||
const res: any = await getAvailableResourceList(props.quotaItemId, queryParams)
|
||||
console.log('getAvailableResourceList==', res)
|
||||
|
||||
const dataList = Array.isArray(res) ? res : (res?.list || [])
|
||||
const total = Array.isArray(res) ? res.length : (res?.total || 0)
|
||||
|
||||
popoverPagination.total = total
|
||||
popoverPagination.hasMore = (popoverPagination.pageNo * popoverPagination.pageSize) < popoverPagination.total
|
||||
|
||||
if (append) {
|
||||
// 追加数据(滚动加载)
|
||||
popoverTableData.value = [...popoverTableData.value, ...dataList]
|
||||
} else {
|
||||
// 替换数据(首次加载或搜索)
|
||||
popoverTableData.value = dataList
|
||||
}
|
||||
} finally {
|
||||
popoverPagination.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部加载更多
|
||||
const handleTableEndReached = () => {
|
||||
if (popoverPagination.hasMore && !popoverPagination.loading) {
|
||||
popoverPagination.pageNo++
|
||||
getTableDataList({}, true) // append = true
|
||||
}
|
||||
}
|
||||
const unitLabelByValue = computed(() => new Map(getDictOptions(DICT_TYPE.MATERIAL_UNIT, 'string').map((option) => [option.value, option.label])))
|
||||
|
||||
const columnsStyle = {
|
||||
borderLeft: '1px solid #e4e4e7',
|
||||
background: '#f0f0f0',
|
||||
color: '#373737',
|
||||
fontWeight: 400
|
||||
}
|
||||
|
||||
// 为 ElTableV2 定义 columns
|
||||
const popoverColumns = computed(() => [
|
||||
{ key: 'code', dataKey: 'code', title: '编码', width: 120 },
|
||||
{ key: 'name', dataKey: 'name', title: '名称', width: 150 },
|
||||
{ key: 'spec', dataKey: 'spec', title: '型号规格', width: 120 },
|
||||
{ key: 'type', dataKey: 'type', title: '类别', width: 60 },
|
||||
{ key: 'unit', dataKey: 'unit', title: '单位', width: 60, cellRenderer: ({ cellData }: { cellData: any }) => {
|
||||
const label = cellData == null ? '' : unitLabelByValue.value.get(cellData) ?? String(cellData)
|
||||
return h('span', label)
|
||||
} },
|
||||
{ key: 'taxRate', dataKey: 'taxRate', title: '税率', width: 60 },
|
||||
{ key: 'taxExclBasePrice', dataKey: 'taxExclBasePrice', title: '除税基价', width: 90 },
|
||||
{ key: 'taxInclBasePrice', dataKey: 'taxInclBasePrice', title: '含税基价', width: 90 },
|
||||
{ key: 'taxExclCompilePrice', dataKey: 'taxExclCompilePrice', title: '除税编制价', width: 90 },
|
||||
{ key: 'taxInclCompilePrice', dataKey: 'taxInclCompilePrice', title: '含税编制价', width: 90 },
|
||||
].map(item => ({
|
||||
...item,
|
||||
style: {
|
||||
...columnsStyle,
|
||||
...(item.key === 'taxInclCompilePrice' ? { borderRight: '1px solid #e4e4e7' } : {})
|
||||
}
|
||||
})))
|
||||
const rowEventHandlers: RowEventHandlers = {
|
||||
onClick: ({ rowIndex, event, rowData }) => {
|
||||
console.log('点击了行:', rowIndex, rowData, itemSelectPopover.value)
|
||||
event.stopPropagation()
|
||||
visible.value = false
|
||||
onClickOutside()
|
||||
|
||||
const { row, currentRowData, instance } = itemSelectPopover.value
|
||||
// 使用 setDataAtRowProp 的数组格式批量更新(一次调用,避免多次渲染)
|
||||
// 格式: [[row, prop, value], [row, prop, value], ...]
|
||||
setTimeout(async () => {
|
||||
instance.setDataAtRowProp([
|
||||
[row, 'code', rowData.code || ''],
|
||||
[row, 'name', rowData.name || ''],
|
||||
[row, 'spec', rowData.spec || ''],
|
||||
[row, 'unit', rowData.unit || ''],
|
||||
[row, 'category', rowData.type || ''], // 使用 type 字段(material/labor/machine)
|
||||
[row, 'taxRate', rowData.taxRate || 0],
|
||||
[row, 'basePriceExTax', rowData.taxExclBasePrice || 0],
|
||||
[row, 'basePriceInTax', rowData.taxInclBasePrice || 0],
|
||||
[row, 'compilePriceExTax', rowData.taxExclCompilePrice || 0],
|
||||
[row, 'compilePriceInTax', rowData.taxInclCompilePrice || 0],
|
||||
[row, 'resourceItemId', rowData.id],
|
||||
], 'selectResource')
|
||||
instance.render()
|
||||
|
||||
// 如果是新行(没有ID),调用创建接口
|
||||
if (currentRowData.id == null) {
|
||||
try {
|
||||
const createData = {
|
||||
quotaItemId: props.quotaItemId,
|
||||
resourceItemId: rowData.id,
|
||||
dosage: currentRowData.quotaQuantity || 0,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const newId = await createQuotaResource(createData)
|
||||
console.log('创建定额工料机组成成功, ID:', newId)
|
||||
|
||||
// 更新行ID
|
||||
instance.setDataAtRowProp([
|
||||
[row, 'id', newId],
|
||||
], 'updateId')
|
||||
|
||||
ElMessage.success('添加成功')
|
||||
} catch (error) {
|
||||
console.error('创建定额工料机组成失败:', error)
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
} else {
|
||||
// 如果已有ID,调用更新接口
|
||||
try {
|
||||
const updateData = {
|
||||
id: currentRowData.id,
|
||||
quotaItemId: props.quotaItemId,
|
||||
resourceItemId: rowData.id,
|
||||
dosage: currentRowData.quotaQuantity || 0,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
await updateQuotaResource(updateData)
|
||||
console.log('更新定额工料机组成成功')
|
||||
ElMessage.success('更新成功')
|
||||
} catch (error) {
|
||||
console.error('更新定额工料机组成失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
}
|
||||
|
||||
const popoverTableData = ref<any[]>([])
|
||||
const selectRender = createPopoverCellRenderer({
|
||||
visible,
|
||||
buttonRef,
|
||||
})
|
||||
const { load, codeRenderer, handleRowOperation } = useParentChildLineNestedRowsFalse({
|
||||
getHotInstance: () => hstRef.value.hotInstance,
|
||||
})
|
||||
const itemSelectPopover = ref<any>()
|
||||
|
||||
// 工料机类型映射
|
||||
const resourceTypeMap = new Map([
|
||||
['material', '材料'],
|
||||
['labor', '人工'],
|
||||
['machine', '机械']
|
||||
])
|
||||
|
||||
const mockData = () => {
|
||||
const categories = ['人', '人机', '材', '机']
|
||||
const units = ['工日', 't', 'm³', 'kg', 'm²']
|
||||
return Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `SUB${String(index + 1).padStart(6, '0')}`,
|
||||
name: `子目项${index + 1}`,
|
||||
spec: `规格${index + 1}`,
|
||||
unit: units[index % units.length],
|
||||
category: categories[index % categories.length],
|
||||
taxRate: `${(Math.random() * 5 + 8).toFixed(2)}%`,
|
||||
basePriceExTax: (Math.random() * 1000 + 100).toFixed(2),
|
||||
basePriceInTax: (Math.random() * 1100 + 110).toFixed(2),
|
||||
compilePriceExTax: (Math.random() * 1200 + 120).toFixed(2),
|
||||
compilePriceInTax: (Math.random() * 1300 + 130).toFixed(2),
|
||||
quotaQuantity: (Math.random() * 10 + 1).toFixed(3),
|
||||
adjustQuantity: (Math.random() * 10 + 1).toFixed(3)
|
||||
}))
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer, allowInvalid: true, autoWidth: true },
|
||||
{
|
||||
type: 'text',
|
||||
data: 'name',
|
||||
title: '名称',
|
||||
readOnly: true,
|
||||
renderer: selectRender,
|
||||
focusCallback: async (row: any, _col: any, _prop: any, _value: any, rowData: any, instance: any, _td: any) => {
|
||||
// 获得焦点时加载第一页数据
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({}, false)
|
||||
console.log('focusCallback-popoverTableData 已加载:', popoverTableData.value)
|
||||
itemSelectPopover.value = { row, currentRowData: rowData, instance }
|
||||
},
|
||||
selectCallback: async (row: any, _col: any, _prop: any, _value: any, rowData: any, instance: any, _td: any) => {
|
||||
// 点击搜索图标时重置分页并加载第一页
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({}, false)
|
||||
console.log('selectCallback-popoverTableData 已更新:', popoverTableData.value)
|
||||
itemSelectPopover.value = { row, currentRowData: rowData, instance }
|
||||
},
|
||||
inputCallback: async (row: any, _col: any, _prop: any, value: any, rowData: any, instance: any, _td: any) => {
|
||||
// 输入时重置分页并根据输入值过滤数据
|
||||
popoverPagination.pageNo = 1
|
||||
popoverPagination.hasMore = true
|
||||
await getTableDataList({ name: value.trim() }, false)
|
||||
console.log('inputCallback 根据输入过滤后的数据:', popoverTableData.value)
|
||||
itemSelectPopover.value = { row, currentRowData: rowData, instance }
|
||||
}
|
||||
},
|
||||
{ type: 'text', data: 'spec', title: '型号规格', readOnly: true },
|
||||
{
|
||||
type: 'text',
|
||||
data: 'unit',
|
||||
title: '单位',
|
||||
readOnly: true,
|
||||
renderer: function(instance: any, td: any, row: any, col: any, prop: any, value: any, cellProperties: any) {
|
||||
// 使用字典将单位值转换为标签
|
||||
const label = value == null ? '' : unitLabelByValue.value.get(value) ?? String(value)
|
||||
td.innerHTML = label
|
||||
return td
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
data: 'category',
|
||||
title: '类别',
|
||||
readOnly: true,
|
||||
renderer: function(instance: any, td: any, row: any, col: any, prop: any, value: any, cellProperties: any) {
|
||||
// 将 resourceType 转换为中文
|
||||
const label = value == null ? '' : resourceTypeMap.get(value) ?? String(value)
|
||||
td.innerHTML = label
|
||||
return td
|
||||
}
|
||||
},
|
||||
{ type: 'numeric', data: 'taxRate', title: '税率', readOnly: true },
|
||||
{ type: 'numeric', data: 'basePriceExTax', title: '除税基价', readOnly: true },
|
||||
{ type: 'numeric', data: 'basePriceInTax', title: '含税基价', readOnly: true },
|
||||
{ type: 'numeric', data: 'compilePriceExTax', title: '除税编制价', readOnly: true },
|
||||
{ type: 'numeric', data: 'compilePriceInTax', title: '含税编制价', readOnly: true },
|
||||
{ type: 'numeric', data: 'quotaQuantity', title: '定额消耗量' },
|
||||
{ type: 'numeric', data: 'adjustQuantity', title: '调整消耗量' },
|
||||
{ type: 'numeric', data: 'totalBasePriceExTax', title: '除税基价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'totalBasePriceInTax', title: '含税基价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'totalCompilePriceExTax', title: '除税编制价合价', readOnly: true },
|
||||
{ type: 'numeric', data: 'totalCompilePriceInTax', title: '含税编制价合价', readOnly: true },
|
||||
{ type: 'text', data: 'calcBase.formula', title: '计算基数', readOnly: true }
|
||||
])
|
||||
|
||||
// 递归转换数据,将 mergedItems 转换为 __children
|
||||
const transformItem = (item: any): any => {
|
||||
const transformed: any = {
|
||||
...item,
|
||||
code: item.resourceCode || '', // 工料机编码
|
||||
name: item.resourceName || '', // 工料机名称
|
||||
spec: item.resourceSpec || '', // 型号规格
|
||||
unit: item.resourceUnit || '', // 工料机单位
|
||||
category: item.resourceType || '', // 类别:使用 resourceType(material/labor/machine)
|
||||
taxRate: item.resourceTaxRate || 0, // 税率
|
||||
basePriceExTax: item.resourceTaxExclBasePrice || 0, // 除税基价
|
||||
basePriceInTax: item.resourceTaxInclBasePrice || 0, // 含税基价
|
||||
compilePriceExTax: item.resourceTaxExclCompilePrice || 0, // 除税编制价
|
||||
compilePriceInTax: item.resourceTaxInclCompilePrice || 0, // 含税编制价
|
||||
quotaQuantity: item.dosage || 0, // 定额消耗量
|
||||
adjustQuantity: item.attributes?.adjustQuantity || 0, // 调整消耗量
|
||||
// 虚拟字段:合价 = 单价 × 定额消耗量
|
||||
totalBasePriceExTax: Number(((item.resourceTaxExclBasePrice || 0) * (item.dosage || 0)).toFixed(2)), // 除税基价合价
|
||||
totalBasePriceInTax: Number(((item.resourceTaxInclBasePrice || 0) * (item.dosage || 0)).toFixed(2)), // 含税基价合价
|
||||
totalCompilePriceExTax: Number(((item.resourceTaxExclCompilePrice || 0) * (item.dosage || 0)).toFixed(2)), // 除税编制价合价
|
||||
totalCompilePriceInTax: Number(((item.resourceTaxInclCompilePrice || 0) * (item.dosage || 0)).toFixed(2)), // 含税编制价合价
|
||||
calcBase: item.calcBase || {}, // 计算基数
|
||||
resourceItemId: item.resourceItemId, // 保存原始工料机项ID
|
||||
quotaItemId: item.quotaItemId, // 保存定额子目ID
|
||||
attributes: item.attributes || {},
|
||||
isMerged: item.isMerged,
|
||||
parent: false
|
||||
}
|
||||
// 如果存在 mergedItems,递归转换为 __children
|
||||
if (item.mergedItems && Array.isArray(item.mergedItems) && item.mergedItems.length > 0) {
|
||||
transformed.__children = item.mergedItems.map(transformItem)
|
||||
}
|
||||
|
||||
return transformed
|
||||
}
|
||||
|
||||
// 加载工料机组成列表
|
||||
const loadQuotaResources = async (quotaItemId: number) => {
|
||||
try {
|
||||
console.log('SubItemMaterials: 开始加载工料机组成列表, quotaItemId:', quotaItemId)
|
||||
const res = await getQuotaResourceList(quotaItemId)
|
||||
console.log('SubItemMaterials: 工料机组成列表原始数据:', res)
|
||||
|
||||
if (res && Array.isArray(res)) {
|
||||
// 转换数据格式以匹配表格列,并递归处理 mergedItems
|
||||
const transformedData = res.map(transformItem)
|
||||
// 计算合价
|
||||
transformedData.forEach(item => {
|
||||
// 除税基价合价 = 除税基价 * 定额消耗量
|
||||
item.priceExTax5 = Number((item.priceExTax * item.priceExTax3).toFixed(2))
|
||||
|
||||
// 如果有损耗率,需要考虑损耗
|
||||
const lossRate = item.attributes?.loss_rate || 0
|
||||
if (lossRate > 0) {
|
||||
item.priceExTax5 = Number((item.priceExTax * item.priceExTax3 * (1 + lossRate)).toFixed(2))
|
||||
}
|
||||
item.parent = true
|
||||
})
|
||||
//quotaResourcesData.value = transformedData
|
||||
console.log('SubItemMaterials: 工料机组成列表转换后数据:', transformedData)
|
||||
load(transformedData)
|
||||
} else {
|
||||
console.warn('SubItemMaterials: 工料机组成列表数据为空或格式不正确')
|
||||
load([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SubItemMaterials: 加载工料机组成列表失败:', error)
|
||||
load([])
|
||||
}
|
||||
}
|
||||
let rowSchema: any = {__id: null, level: null,__children: [], calcBase: {}}
|
||||
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data && col.data !== 'level' && col.data !== '__children' && col.data !== '__id') {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
const settings = {
|
||||
data: mockData(),
|
||||
data: [], // 初始为空,等待从接口加载
|
||||
dataSchema: rowSchema,
|
||||
columns: columns.value,
|
||||
colWidths: 120,
|
||||
height: 270
|
||||
height: 270,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: {
|
||||
items: {
|
||||
custom_row_above: {
|
||||
name: '在上方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'above')
|
||||
}
|
||||
},
|
||||
custom_row_below: {
|
||||
name: '在下方插入行',
|
||||
callback: function() {
|
||||
handleRowOperation(this, 'below')
|
||||
}
|
||||
},
|
||||
// separator1: '---------',
|
||||
// custom_add_child: {
|
||||
// name: '添加子行',
|
||||
// callback: function() {
|
||||
// handleRowOperation(this, 'child')
|
||||
// }
|
||||
// },
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function() {
|
||||
let selected = this.getSelected()
|
||||
const row = selected[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// console.log(rowData)
|
||||
if (rowData?.id) {
|
||||
// 调用删除 API
|
||||
deleteQuotaResource(rowData.id).then(res => {
|
||||
console.log('删除定额工料机组成成功', res)
|
||||
ElMessage.success('删除成功')
|
||||
handleRowOperation(this, 'delete')
|
||||
}).catch(err => {
|
||||
console.error('删除定额工料机组成失败:', err)
|
||||
ElMessage.error('删除失败')
|
||||
})
|
||||
} else {
|
||||
// 没有 ID 直接删除
|
||||
handleRowOperation(this, 'delete')
|
||||
}
|
||||
}
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
afterChange(changes: any, source: string) {
|
||||
console.log('afterChange', changes, source)
|
||||
|
||||
// 忽略特定来源的变更
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'selectResource' || source === 'updateTotal') {
|
||||
return
|
||||
}
|
||||
|
||||
// 只处理 quotaQuantity 和 adjustQuantity 字段的变更
|
||||
changes.forEach(([row, prop, oldValue, newValue]: any) => {
|
||||
if (prop === 'quotaQuantity' || prop === 'adjustQuantity') {
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// 当定额消耗量改变时,重新计算合价
|
||||
if (prop === 'quotaQuantity') {
|
||||
const dosage = newValue || 0
|
||||
const basePriceExTax = rowData.basePriceExTax || 0
|
||||
const basePriceInTax = rowData.basePriceInTax || 0
|
||||
const compilePriceExTax = rowData.compilePriceExTax || 0
|
||||
const compilePriceInTax = rowData.compilePriceInTax || 0
|
||||
|
||||
// 更新合价字段
|
||||
this.setDataAtRowProp([
|
||||
[row, 'totalBasePriceExTax', Number((basePriceExTax * dosage).toFixed(2))],
|
||||
[row, 'totalBasePriceInTax', Number((basePriceInTax * dosage).toFixed(2))],
|
||||
[row, 'totalCompilePriceExTax', Number((compilePriceExTax * dosage).toFixed(2))],
|
||||
[row, 'totalCompilePriceInTax', Number((compilePriceInTax * dosage).toFixed(2))]
|
||||
], 'updateTotal')
|
||||
}
|
||||
|
||||
// 如果有ID,调用更新接口
|
||||
if (rowData?.id && props.quotaItemId) {
|
||||
const updateData = {
|
||||
id: rowData.id,
|
||||
quotaItemId: props.quotaItemId,
|
||||
resourceItemId: rowData.resourceItemId,
|
||||
dosage: rowData.quotaQuantity || 0,
|
||||
attributes: {
|
||||
adjustQuantity: rowData.adjustQuantity || 0
|
||||
}
|
||||
}
|
||||
|
||||
updateQuotaResource(updateData).then(() => {
|
||||
console.log('更新定额消耗量成功')
|
||||
}).catch(err => {
|
||||
console.error('更新定额消耗量失败:', err)
|
||||
ElMessage.error('更新失败')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// 计算属性:根据是否选择了定额子目来决定是否显示右键菜单
|
||||
const contextMenuItems = computed(() => {
|
||||
// 如果没有选择定额子目,返回空数组(不显示右键菜单)
|
||||
if (!props.quotaItemId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 如果选择了定额子目,返回完整的右键菜单
|
||||
return [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
handleRowOperation(hotInstance, 'append')
|
||||
|
||||
// 等待 DOM 更新后重新渲染以应用验证样式
|
||||
nextTick(() => {
|
||||
hotInstance.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const clear = ()=>{
|
||||
load([])
|
||||
}
|
||||
// 监听 quotaItemId 变化
|
||||
/*watch(
|
||||
() => props.quotaItemId,
|
||||
(oldQuotaItemId,newQuotaItemId) => {
|
||||
console.log('SubItemMaterials: quotaItemId 变化:', oldQuotaItemId,newQuotaItemId)
|
||||
if (newQuotaItemId) {
|
||||
// loadQuotaResources(newQuotaItemId)
|
||||
} else {
|
||||
// 清空数据
|
||||
// quotaResourcesData.value = []
|
||||
if (hstRef.value?.hotInstance) {
|
||||
// hstRef.value.hotInstance.loadData([])
|
||||
load([])
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{ immediate: true,deep: true }
|
||||
)*/
|
||||
|
||||
watch(
|
||||
() => props.height,
|
||||
@@ -55,7 +510,7 @@ watch(
|
||||
console.log('SubItemMaterials newHeight', newHeight)
|
||||
if (newHeight && hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
})
|
||||
hstRef.value.hotInstance.render()
|
||||
}
|
||||
@@ -64,17 +519,29 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
console.log('SubItemMaterials mounted')
|
||||
console.log('SubItemMaterials mounted, quotaItemId:', props.quotaItemId)
|
||||
if(props.quotaItemId){
|
||||
loadQuotaResources(props.quotaItemId)
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ hstRef })
|
||||
defineExpose({ hstRef, loadQuotaResources, clear })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
<div class="w-full h-full" @contextmenu.prevent>
|
||||
<DbHst ref="hstRef" :settings="settings" :contextMenuItems="contextMenuItems" />
|
||||
</div>
|
||||
<DbHstPopover
|
||||
ref="popoverComponentRef"
|
||||
:visible="visible"
|
||||
:button-ref="buttonRef"
|
||||
:columns="popoverColumns"
|
||||
:data="popoverTableData"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
@end-reached="handleTableEndReached"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||
import ElementPlus from 'unplugin-element-plus/vite';
|
||||
|
||||
export default defineConfig(async () => {
|
||||
@@ -10,6 +11,9 @@ export default defineConfig(async () => {
|
||||
ElementPlus({
|
||||
format: 'esm',
|
||||
}),
|
||||
codeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"@vitejs/plugin-vue-jsx": "catalog:",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"autoprefixer": "catalog:",
|
||||
"code-inspector-plugin": "^1.3.4",
|
||||
"cross-env": "catalog:",
|
||||
"cspell": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
|
||||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
@@ -656,6 +656,9 @@ importers:
|
||||
autoprefixer:
|
||||
specifier: 'catalog:'
|
||||
version: 10.4.22(postcss@8.5.6)
|
||||
code-inspector-plugin:
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.3
|
||||
@@ -2689,6 +2692,24 @@ packages:
|
||||
resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@code-inspector/core@1.3.4':
|
||||
resolution: {integrity: sha512-AUFtDH/hngBHrNVwW1z44ogZkaGhfmFQJZjyQrSCp+mERVQqa4QNGQpRqiIEWVCwJ2e3GCuwxeAr/k48r1nscA==}
|
||||
|
||||
'@code-inspector/esbuild@1.3.4':
|
||||
resolution: {integrity: sha512-VVZLPnaUNtX4fm07bKBkdIn2t1H0jPzah3fHd/CtNO82mnT1H3hZCuvtlO4jQZnErWC1KYJUEDrEe4T5TA5giQ==}
|
||||
|
||||
'@code-inspector/mako@1.3.4':
|
||||
resolution: {integrity: sha512-SvjZSfLXgiWDMmJ9+YfPqbE2WVbXCNPjREclEJfqkM2qS/oRPmHw/O81p5wh6dN48gzVadLKEOPtSE0+FURJgQ==}
|
||||
|
||||
'@code-inspector/turbopack@1.3.4':
|
||||
resolution: {integrity: sha512-zsv2ppMFedNZetrlN4PEW4B2vAheu3yUfrmSKfZlXEb8YT378sq+49+57aP/E1Q8cHRzowy4GItuPKwAy7TTVQ==}
|
||||
|
||||
'@code-inspector/vite@1.3.4':
|
||||
resolution: {integrity: sha512-BcRnQFwt8yQ4CcbN7yPf/Vmon3yfS5lPpcH0QAcjD03r61if5fmix29f65Rf/WxzlVyBkk7148xaIVQ3iT2Yjg==}
|
||||
|
||||
'@code-inspector/webpack@1.3.4':
|
||||
resolution: {integrity: sha512-lqsDOSmKXgOYvlurWL4SHaNItNCNZDVbFJlroM4ECnOGt/iNUI0UwaFxn9U5NeKff7fnTmdH8Hrz2IaMR4FVTg==}
|
||||
|
||||
'@codemirror/autocomplete@6.20.0':
|
||||
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
|
||||
|
||||
@@ -5496,6 +5517,10 @@ packages:
|
||||
resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
chalk@4.1.1:
|
||||
resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5614,6 +5639,9 @@ packages:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
code-inspector-plugin@1.3.4:
|
||||
resolution: {integrity: sha512-735rAAc655oSAMd/6+PIsjpgB5jwugDISom9WFwhNUbOuFHiL2PYwshMmfIhAtOgECl+7g6o50rBIIYwCEa8xg==}
|
||||
|
||||
codemirror@5.65.20:
|
||||
resolution: {integrity: sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==}
|
||||
|
||||
@@ -7788,6 +7816,9 @@ packages:
|
||||
resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
launch-ide@1.3.1:
|
||||
resolution: {integrity: sha512-opTthrpkuhi1Y8yFn6TWUeycyiI1aiZpVuTV4HQFUfVut7nMYGr5nQ8heYHrRJH2KYISLVYwz+QFRNZxFlbQmA==}
|
||||
|
||||
lazystream@1.0.1:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
@@ -8724,6 +8755,10 @@ packages:
|
||||
popmotion@11.0.5:
|
||||
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
|
||||
|
||||
portfinder@1.0.38:
|
||||
resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==}
|
||||
engines: {node: '>= 10.12'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -12059,6 +12094,48 @@ snapshots:
|
||||
dependencies:
|
||||
mime: 3.0.0
|
||||
|
||||
'@code-inspector/core@1.3.4':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.25
|
||||
chalk: 4.1.2
|
||||
dotenv: 16.6.1
|
||||
launch-ide: 1.3.1
|
||||
portfinder: 1.0.38
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/esbuild@1.3.4':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/mako@1.3.4':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/turbopack@1.3.4':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
'@code-inspector/webpack': 1.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/vite@1.3.4':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
chalk: 4.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/webpack@1.3.4':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@codemirror/autocomplete@6.20.0':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
@@ -15179,6 +15256,11 @@ snapshots:
|
||||
dependencies:
|
||||
chalk: 5.6.2
|
||||
|
||||
chalk@4.1.1:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -15336,6 +15418,18 @@ snapshots:
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
code-inspector-plugin@1.3.4:
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.3.4
|
||||
'@code-inspector/esbuild': 1.3.4
|
||||
'@code-inspector/mako': 1.3.4
|
||||
'@code-inspector/turbopack': 1.3.4
|
||||
'@code-inspector/vite': 1.3.4
|
||||
'@code-inspector/webpack': 1.3.4
|
||||
chalk: 4.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
codemirror@5.65.20: {}
|
||||
|
||||
codemirror@6.65.7: {}
|
||||
@@ -17678,6 +17772,11 @@ snapshots:
|
||||
dependencies:
|
||||
package-json: 10.0.1
|
||||
|
||||
launch-ide@1.3.1:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
dotenv: 16.6.1
|
||||
|
||||
lazystream@1.0.1:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
@@ -18672,6 +18771,13 @@ snapshots:
|
||||
style-value-types: 5.1.2
|
||||
tslib: 2.4.0
|
||||
|
||||
portfinder@1.0.38:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-antd-fixes@0.2.0(postcss@8.5.6):
|
||||
|
||||
Reference in New Issue
Block a user