工料机、定额基价、定额费率、定额取费
This commit is contained in:
@@ -4,7 +4,9 @@ VITE_PORT=5777
|
|||||||
VITE_BASE=/
|
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
|
VITE_GLOB_API_URL=/admin-api
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型: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 = (
|
const openDropdown = (
|
||||||
td: HTMLTableCellElement,
|
td: HTMLTableCellElement,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
source: string[],
|
source: any[],
|
||||||
onSelect: (opt: string) => void,
|
onSelect: (opt: string) => void,
|
||||||
isOptionDisabled?: (opt: string) => boolean,
|
isOptionDisabled?: (opt: string) => boolean,
|
||||||
|
onAfterSelect?: (oldValue: unknown, newValue: string, optData?: any) => void,
|
||||||
) => {
|
) => {
|
||||||
closeDropdown()
|
closeDropdown()
|
||||||
const menu = document.createElement('div')
|
const menu = document.createElement('div')
|
||||||
menu.className = 'ht-dropdown-menu'
|
menu.className = 'ht-dropdown-menu'
|
||||||
const frag = document.createDocumentFragment()
|
const frag = document.createDocumentFragment()
|
||||||
|
|
||||||
for (const opt of source) {
|
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')
|
const item = document.createElement('div')
|
||||||
item.className = 'ht-dropdown-item'
|
item.className = 'ht-dropdown-item'
|
||||||
item.textContent = opt
|
item.textContent = optLabel ?? ''
|
||||||
if (String(value) === String(opt)) item.classList.add('is-selected')
|
if (String(value) === String(optValue)) item.classList.add('is-selected')
|
||||||
const disabled = isDisabled.value && isOptionDisabled?.(opt) === true
|
const disabled = isDisabled.value && isOptionDisabled?.(String(optValue)) === true
|
||||||
if (disabled) { item.classList.add('is-disabled'); item.setAttribute('aria-disabled', '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)
|
frag.appendChild(item)
|
||||||
}
|
}
|
||||||
menu.appendChild(frag)
|
menu.appendChild(frag)
|
||||||
@@ -51,23 +67,71 @@ const openDropdown = (
|
|||||||
currentOnDocClick = (ev: MouseEvent) => { const target = ev.target as Node; if (currentDropdownEl && !currentDropdownEl.contains(target)) closeDropdown() }
|
currentOnDocClick = (ev: MouseEvent) => { const target = ev.target as Node; if (currentDropdownEl && !currentDropdownEl.contains(target)) closeDropdown() }
|
||||||
document.addEventListener('click', currentOnDocClick, true)
|
document.addEventListener('click', currentOnDocClick, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: any) {
|
export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: any) {
|
||||||
td.innerHTML = ''
|
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')
|
const wrapper = document.createElement('div')
|
||||||
wrapper.className = 'ht-cell-dropdown'
|
wrapper.className = 'ht-cell-dropdown'
|
||||||
const valueEl = document.createElement('span')
|
const valueEl = document.createElement('span')
|
||||||
valueEl.className = 'ht-cell-value'
|
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')
|
const caretEl = document.createElement('span')
|
||||||
caretEl.className = 'ht-cell-caret'
|
caretEl.className = 'ht-cell-caret'
|
||||||
wrapper.appendChild(valueEl)
|
wrapper.appendChild(valueEl)
|
||||||
wrapper.appendChild(caretEl)
|
wrapper.appendChild(caretEl)
|
||||||
td.appendChild(wrapper)
|
td.appendChild(wrapper)
|
||||||
const source: string[] = Array.isArray(cellProperties?.source)
|
|
||||||
? cellProperties.source
|
|
||||||
: Array.isArray(cellProperties?.customDropdownSource)
|
|
||||||
? cellProperties.customDropdownSource
|
|
||||||
: []
|
|
||||||
let disabledSet = new Set<string>()
|
let disabledSet = new Set<string>()
|
||||||
if (isDisabled.value) {
|
if (isDisabled.value) {
|
||||||
const colValues = instance.getSourceDataAtCol(column) as unknown[]
|
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 = new Set((Array.isArray(colValues) ? colValues : []).map(v => String(v)))
|
||||||
disabledSet.delete(currentStr)
|
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
|
return td
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,28 @@ import { HotTable } from '@handsontable/vue3'
|
|||||||
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'
|
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'
|
||||||
import { registerAllModules } from 'handsontable/registry'
|
import { registerAllModules } from 'handsontable/registry'
|
||||||
import 'handsontable/styles/handsontable.css'
|
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()
|
registerAllModules()
|
||||||
registerLanguageDictionary(zhCN)
|
registerLanguageDictionary(zhCN)
|
||||||
import { handlerDropdownRenderer } from './dropdown'
|
import { handlerDropdownRenderer } from './dropdown'
|
||||||
import { handlerTableRenderer } from './table'
|
import { handlerTableRenderer } from './table'
|
||||||
|
import { handlerDuplicateCodeRenderer } from './text'
|
||||||
import { computeCodeColWidth,codeRenderer } from './tree'
|
import { computeCodeColWidth,codeRenderer } from './tree'
|
||||||
|
import ContextMenu from './contextmenu.vue'
|
||||||
|
import { handleRowOperation } from '#/components/db-hst/tree'
|
||||||
// import { sourceDataObject } from './mockData'
|
// import { sourceDataObject } from './mockData'
|
||||||
// const language = ref('zh-CN')
|
// const language = ref('zh-CN')
|
||||||
defineOptions({ name: 'DbHst' });
|
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 { registerCellType, NumericCellType } from 'handsontable/cellTypes';
|
||||||
// import { registerPlugin, UndoRedo } from 'handsontable/plugins';
|
// import { registerPlugin, UndoRedo } from 'handsontable/plugins';
|
||||||
@@ -24,13 +36,15 @@ const componentProps = defineProps<{ settings?: any }>()
|
|||||||
// const tableHeight = computed(() => componentProps.height ?? 0)
|
// const tableHeight = computed(() => componentProps.height ?? 0)
|
||||||
// const defaultData = ref<any[][]>(Array.from({ length: 30 }, () => Array(componentProps.columns?.length ?? 0).fill('')))
|
// const defaultData = ref<any[][]>(Array.from({ length: 30 }, () => Array(componentProps.columns?.length ?? 0).fill('')))
|
||||||
const hotTableComponent = ref<any>(null)
|
const hotTableComponent = ref<any>(null)
|
||||||
|
const selectedRow = ref<number | null>(null) // 记录当前选中的行
|
||||||
const codeColWidth = ref<number>(120)
|
const codeColWidth = ref<number>(120)
|
||||||
|
|
||||||
|
|
||||||
// const colHeaders = ref<string[]>([])
|
// const colHeaders = ref<string[]>([])
|
||||||
let defaultSettings = {
|
let defaultSettings = {
|
||||||
themeName: 'ht-theme-main',
|
// themeName: 'ht-theme-main',
|
||||||
|
themeName: 'ht-theme-classic',
|
||||||
|
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
// data: sourceDataObject,
|
// data: sourceDataObject,
|
||||||
// colWidths: [100, 120, 100, 100, 100, 100],
|
// colWidths: [100, 120, 100, 100, 100, 100],
|
||||||
@@ -40,7 +54,7 @@ let defaultSettings = {
|
|||||||
// return (index + 1) * 40;
|
// return (index + 1) * 40;
|
||||||
// },
|
// },
|
||||||
// colWidths: undefined,
|
// colWidths: undefined,
|
||||||
rowHeights: '23px', // 固定行高
|
rowHeights: 23, // 固定行高
|
||||||
wordWrap: false,// 禁止单元格内容自动换行
|
wordWrap: false,// 禁止单元格内容自动换行
|
||||||
|
|
||||||
//manualColumnMove: true,
|
//manualColumnMove: true,
|
||||||
@@ -90,25 +104,69 @@ let defaultSettings = {
|
|||||||
|
|
||||||
// return cellProperties;
|
// return cellProperties;
|
||||||
// },
|
// },
|
||||||
modifyColWidth: (width: number, col: number) => {
|
// afterSelection(row1: number, _col1: number, _row2: number, _col2: number) {
|
||||||
const hot = hotInstance.value
|
// const hot = this as any
|
||||||
if (!hot) return width
|
// if (selectedRow.value !== null && selectedRow.value !== row1) {
|
||||||
const codeCol = hot.propToCol('code')
|
// const colCount = hot.countCols()
|
||||||
// console.log('modifyColWidth',codeCol,width)
|
// for (let c = 0; c < colCount; c++) {
|
||||||
return col === codeCol ? (codeColWidth.value ?? width) : width
|
// const meta = hot.getCellMeta(selectedRow.value, c)
|
||||||
},
|
// const classes = (meta.className || '').split(' ').filter(Boolean)
|
||||||
afterChange: (changes: any, source: string) => {
|
// const idx = classes.indexOf('row-highlight')
|
||||||
if (!changes || !hotInstance.value) return
|
// if (idx !== -1) classes.splice(idx, 1)
|
||||||
if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return
|
// hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' '))
|
||||||
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)
|
// selectedRow.value = row1
|
||||||
if (!hasCodeEdit) return
|
|
||||||
codeColWidth.value = computeCodeColWidth(hot)
|
// const colCount = hot.countCols()
|
||||||
// console.log('afterChange',codeColWidth.value)
|
// for (let c = 0; c < colCount; c++) {
|
||||||
hot.render()
|
// const meta = hot.getCellMeta(row1, c)
|
||||||
// console.log('afterChange',codeColWidth.value)
|
// 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 和默认配置
|
// 合并外部 settings 和默认配置
|
||||||
@@ -116,6 +174,12 @@ let hotSettings = {}
|
|||||||
// 保留必要的回调函数
|
// 保留必要的回调函数
|
||||||
|
|
||||||
const hotInstance = ref<any>(null)
|
const hotInstance = ref<any>(null)
|
||||||
|
const contextMenuRef = ref<any>(null)
|
||||||
|
|
||||||
|
// 处理右键菜单事件
|
||||||
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
|
contextMenuRef.value?.handleContextMenu(event)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
hotInstance.value = hotTableComponent.value?.hotInstance
|
hotInstance.value = hotTableComponent.value?.hotInstance
|
||||||
@@ -127,15 +191,17 @@ watch(
|
|||||||
() => componentProps.settings,
|
() => componentProps.settings,
|
||||||
(newSettings) => {
|
(newSettings) => {
|
||||||
if (!newSettings) return
|
if (!newSettings) return
|
||||||
|
|
||||||
const merged = {
|
const merged = {
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
...newSettings,
|
...newSettings,
|
||||||
}
|
}
|
||||||
Object.assign(hotSettings, merged)
|
Object.assign(hotSettings, merged)
|
||||||
hotSettings = merged
|
hotSettings = merged
|
||||||
|
|
||||||
// console.log(merged)
|
// console.log(merged)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true,deep:true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadData = (rows: any[][]) => {
|
const loadData = (rows: any[][]) => {
|
||||||
@@ -143,12 +209,51 @@ const loadData = (rows: any[][]) => {
|
|||||||
if (!hotInstance.value) return
|
if (!hotInstance.value) return
|
||||||
// hotInstance.value.loadData(rows.length === 0?defaultData.value:rows)
|
// hotInstance.value.loadData(rows.length === 0?defaultData.value:rows)
|
||||||
hotInstance.value.loadData(rows)
|
hotInstance.value.loadData(rows)
|
||||||
console.log('Source Data:', hotInstance.value.getSourceData());
|
//console.log('Source Data:', hotInstance.value.getSourceData());
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCodeColWidth = () => {
|
const updateCodeColWidth = () => {
|
||||||
if (!hotInstance.value) return
|
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()
|
hotInstance.value.render()
|
||||||
}
|
}
|
||||||
defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, codeColWidth })
|
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-table", handlerTableRenderer);
|
||||||
Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
||||||
|
Handsontable.renderers.registerRenderer("db-duplicate", handlerDuplicateCodeRenderer);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 id="hot-dialog-container" style="display:none">
|
||||||
<div class="ht-dialog-content">
|
<div class="ht-dialog-content">
|
||||||
<h3>执行操作</h3>
|
<h3>执行操作</h3>
|
||||||
@@ -214,14 +323,14 @@ Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
|
|||||||
.ht_master .wtHolder::-webkit-scrollbar-thumb:hover {
|
.ht_master .wtHolder::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
} */
|
} */
|
||||||
|
/* 滚动条width */
|
||||||
.ht_master .wtHolder{
|
.ht_master .wtHolder{
|
||||||
/* overflow: hidden !important; */
|
/* overflow: hidden !important; */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #a6a8ac #ecf0f1;
|
scrollbar-color: #a6a8ac #ecf0f1;
|
||||||
}
|
}
|
||||||
/* dropdown */
|
/* 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 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.ht-cell-value:empty::after { content: "\200b"; }
|
.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; }
|
.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 { 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-search-input:focus { border-color: #3b82f6; }
|
||||||
.hot-dropdown-table-wrapper { overflow: auto; flex: 1; }
|
.hot-dropdown-table-wrapper { overflow: auto; flex: 1; }
|
||||||
.hot-dropdown-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
.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: left; }
|
.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-table tbody td { padding: 8px; border-bottom: 1px solid #f3f4f6; color: #374151; }
|
||||||
.hot-dropdown-row { cursor: pointer; }
|
.hot-dropdown-row { cursor: pointer; }
|
||||||
.hot-dropdown-row:hover { background: #f3f4f6; }
|
.hot-dropdown-row:hover { background: #f3f4f6; }
|
||||||
|
|
||||||
/** 指引线line */
|
|
||||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty{
|
/* 整行高亮样式 */
|
||||||
position: relative;
|
.row-highlight {
|
||||||
display: inline-block;
|
background-color: #e9ecfc !important; /* 浅蓝色背景 */
|
||||||
width: 5px;
|
|
||||||
height: 1px;
|
|
||||||
order: -2;
|
|
||||||
}
|
}
|
||||||
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: '';
|
.ht-id-cell {
|
||||||
position: absolute;
|
position: relative !important;
|
||||||
top: -13px;
|
z-index: 3 !important;
|
||||||
height: 26px;
|
overflow: visible !important;
|
||||||
width: 1px;
|
|
||||||
border-left: 1px solid #ababab;
|
|
||||||
}
|
}
|
||||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty::after{
|
|
||||||
content: '';
|
.ht-id-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
width: 16px;
|
right: -14px;
|
||||||
height: 1px;
|
width: 14px;
|
||||||
border-top: 1px solid #ababab;
|
height: 14px;
|
||||||
}
|
display: none;
|
||||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty{
|
cursor: pointer;
|
||||||
padding-left: 7px;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 最后一个 ht_nestingLevel_empty(rowHeader 前面的那个) */
|
.ht-id-cell.current .ht-id-icon,
|
||||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty + .rowHeader {
|
.ht-id-cell.area .ht-id-icon {
|
||||||
/* 通过相邻选择器反向选择 */
|
display: inline-flex;
|
||||||
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_nestingLevel_empty 的那个 */
|
.handsontable {
|
||||||
tbody[role="rowgroup"] tr[role="row"] td .ht_nestingLevel_empty:not(:has(+ .ht_nestingLevel_empty))::before {
|
--ht-tree-line-color: #7c7c7c;
|
||||||
/* height: 13px; */
|
--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>
|
</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)
|
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 dataSource 数据源数组
|
||||||
* @param fieldKeys 字段键数组,按顺序对应表格列
|
* @param fieldKeys 字段键数组,按顺序对应表格列
|
||||||
* @param getLabelFn 获取标签的函数
|
* @param getDisplayText 获取显示文本的函数
|
||||||
* @returns 格式化后的表格行HTML和数据属性
|
* @returns 格式化后的表格行HTML和数据属性
|
||||||
*/
|
*/
|
||||||
export function createTableDataStructure(
|
export function createTableDataStructure(
|
||||||
dataSource: any[],
|
dataSource: any[],
|
||||||
fieldKeys: string[],
|
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 => {
|
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
|
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(' ')
|
.join(' ')
|
||||||
|
|
||||||
|
// 将完整的 item 数据序列化存储
|
||||||
|
const itemDataJson = JSON.stringify(item).replace(/"/g, '"')
|
||||||
|
|
||||||
return {
|
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
|
data: item
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -93,21 +124,18 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
|||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFn: ((x: any) => string) | undefined = cellProperties.customGetLabel
|
|
||||||
|
|
||||||
const display = createEl('div', 'hot-dropdown-display')
|
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))
|
if (labelText && labelText.length > 0) display.appendChild(createEl('span', 'hot-dropdown-text', labelText))
|
||||||
else display.appendChild(createEl('span', 'hot-dropdown-placeholder', '选择'))
|
else display.appendChild(createEl('span', 'hot-dropdown-placeholder', '选择'))
|
||||||
|
|
||||||
const trigger = createEl('button', 'hot-dropdown-trigger', '▼') as HTMLButtonElement
|
const trigger = createEl('button', 'hot-dropdown-trigger', '▼') as HTMLButtonElement
|
||||||
trigger.type = 'button'
|
trigger.type = 'button'
|
||||||
|
|
||||||
const buildDropdown = () => {
|
const buildDropdown = async () => {
|
||||||
const src = cellProperties.customDropdownSource as any[] | undefined
|
|
||||||
const headers: string[] | undefined = cellProperties.customTableHeaders
|
const headers: string[] | undefined = cellProperties.customTableHeaders
|
||||||
const dropdown = createEl('div', 'hot-dropdown')
|
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')
|
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 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 fieldKeys = cellProperties.customFieldKeys || []
|
||||||
|
|
||||||
const rowsHtml = Array.isArray(src)
|
|
||||||
? createTableDataStructure(src, fieldKeys, labelFn).map(row => row.html).join('')
|
|
||||||
: ''
|
|
||||||
|
|
||||||
|
// 创建加载提示
|
||||||
const tableEl = createEl('div', 'hot-dropdown-table-wrapper')
|
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(searchContainer)
|
||||||
dropdown.appendChild(tableEl)
|
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']
|
const searchFieldKeys = cellProperties.customSearchFields || cellProperties.customFieldKeys || ['code', 'name', 'spec', 'category', 'unit']
|
||||||
searchInput.addEventListener('input', (ev) => {
|
searchInput.addEventListener('input', (ev) => {
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
const keyword = searchInput.value.toLowerCase().trim()
|
const keyword = searchInput.value.toLowerCase().trim()
|
||||||
allRows.forEach(tr => {
|
allRows.forEach(tr => {
|
||||||
const matches = searchFieldKeys.some((key: string) => {
|
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)
|
return value.includes(keyword)
|
||||||
})
|
})
|
||||||
tr.style.display = matches ? '' : 'none'
|
tr.style.display = matches ? '' : 'none'
|
||||||
@@ -154,9 +204,25 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
|||||||
tbody.addEventListener('click', (ev) => {
|
tbody.addEventListener('click', (ev) => {
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
const tr = (ev.target as HTMLElement).closest('tr') as HTMLTableRowElement | null
|
const tr = (ev.target as HTMLElement).closest('tr') as HTMLTableRowElement | null
|
||||||
if (!tr) return
|
if (!tr || !tr.classList.contains('hot-dropdown-row')) return
|
||||||
const next = tr.dataset.label ?? ''
|
|
||||||
instance.setDataAtCell(row, column, next)
|
// 从 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)
|
if (dropdown.parentNode) dropdown.parentNode.removeChild(dropdown)
|
||||||
currentDropdownEl = null
|
currentDropdownEl = null
|
||||||
if (currentOnDocClick) {
|
if (currentOnDocClick) {
|
||||||
@@ -171,11 +237,11 @@ export function handlerTableRenderer(instance: any, td: HTMLTableCellElement, ro
|
|||||||
return dropdown
|
return dropdown
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDropdown = () => {
|
const openDropdown = async () => {
|
||||||
if (currentDropdownEl && currentDropdownEl.parentNode) currentDropdownEl.parentNode.removeChild(currentDropdownEl)
|
if (currentDropdownEl && currentDropdownEl.parentNode) currentDropdownEl.parentNode.removeChild(currentDropdownEl)
|
||||||
if (currentOnDocClick) document.removeEventListener('click', currentOnDocClick)
|
if (currentOnDocClick) document.removeEventListener('click', currentOnDocClick)
|
||||||
|
|
||||||
const dropdown = buildDropdown()
|
const dropdown = await buildDropdown()
|
||||||
document.body.appendChild(dropdown)
|
document.body.appendChild(dropdown)
|
||||||
currentDropdownEl = dropdown
|
currentDropdownEl = dropdown
|
||||||
currentAnchor = { instance, row, col: column, td }
|
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 = (
|
export const codeRenderer = (
|
||||||
hot: any,
|
hot: any,
|
||||||
TD: HTMLTableCellElement,
|
TD: HTMLTableCellElement,
|
||||||
@@ -7,43 +42,71 @@ export const codeRenderer = (
|
|||||||
value: any,
|
value: any,
|
||||||
cellProperties: 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 nestedRowsPlugin = hot.getPlugin('nestedRows')
|
||||||
const physicalRow = nestedRowsPlugin ? nestedRowsPlugin.collapsingUI.translateTrimmedRow(row) : hot.toPhysicalRow(row)
|
const physicalRow = nestedRowsPlugin ? nestedRowsPlugin.collapsingUI.translateTrimmedRow(row) : hot.toPhysicalRow(row)
|
||||||
const rowObj = hot.getSourceDataAtRow(physicalRow)
|
const dataManager = nestedRowsPlugin?.dataManager
|
||||||
const container = document.createElement('div')
|
|
||||||
container.style.display = 'flex'
|
const dataNode = dataManager?.getDataObject?.(physicalRow)
|
||||||
container.style.alignItems = 'center'
|
const root = (dataManager?.getData?.() as any[] | undefined) ?? []
|
||||||
container.style.gap = '6px'
|
const line = nestedRowsPlugin && dataManager && dataNode && Array.isArray(root)
|
||||||
const level = nestedRowsPlugin?.dataManager.getRowLevel(physicalRow) ?? 0
|
? getTreeLine(dataNode, dataManager, root)
|
||||||
for (let i = 0; i < (level || 0); i++) {
|
: ({ key: '0', hasChildren: false } as const)
|
||||||
const spacer = document.createElement('span')
|
|
||||||
spacer.className = 'ht_nestingLevel_empty'
|
const { indentLayer, content, toggleEl, textEl } = getTreeCellDom(TD)
|
||||||
container.appendChild(spacer)
|
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) {
|
|
||||||
|
if (line.hasChildren && nestedRowsPlugin) {
|
||||||
const isCollapsed = nestedRowsPlugin?.collapsingUI.areChildrenCollapsed(physicalRow) ?? false
|
const isCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(physicalRow)
|
||||||
const btn = document.createElement('div')
|
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.className = `ht_nestingButton ${isCollapsed ? 'ht_nestingExpand' : 'ht_nestingCollapse'}`
|
||||||
btn.addEventListener('mousedown', (ev) => {
|
btn.dataset.row = String(physicalRow)
|
||||||
if (ev.button !== 0) return
|
if (!(btn as any).__htTreeToggleBound) {
|
||||||
ev.stopPropagation()
|
;(btn as any).__htTreeToggleBound = true
|
||||||
ev.preventDefault()
|
btn.addEventListener('mousedown', (ev) => {
|
||||||
if (!nestedRowsPlugin) return
|
ev.stopPropagation()
|
||||||
const nowCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(physicalRow)
|
ev.preventDefault()
|
||||||
if (nowCollapsed) nestedRowsPlugin.collapsingUI.expandChildren(physicalRow)
|
const rowStr = (ev.currentTarget as HTMLElement | null)?.dataset.row
|
||||||
else nestedRowsPlugin.collapsingUI.collapseChildren(physicalRow)
|
const targetRow = rowStr ? Number(rowStr) : NaN
|
||||||
})
|
if (!nestedRowsPlugin || Number.isNaN(targetRow)) return
|
||||||
container.appendChild(btn)
|
const nowCollapsed = nestedRowsPlugin.collapsingUI.areChildrenCollapsed(targetRow)
|
||||||
}/*else{
|
if (nowCollapsed) nestedRowsPlugin.collapsingUI.expandChildren(targetRow)
|
||||||
container.classList.add('text-relative')
|
else nestedRowsPlugin.collapsingUI.collapseChildren(targetRow)
|
||||||
}*/
|
})
|
||||||
const text = document.createElement('span')
|
}
|
||||||
text.textContent = value == null ? '' : String(value)
|
if (needsButton) content.replaceChild(btn, toggleEl)
|
||||||
text.classList.add('rowHeader')
|
} 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.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
const e = new MouseEvent('contextmenu', {
|
const e = new MouseEvent('contextmenu', {
|
||||||
@@ -56,19 +119,52 @@ export const codeRenderer = (
|
|||||||
})
|
})
|
||||||
TD.dispatchEvent(e)
|
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 => {
|
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 data = hot.getSourceData() || []
|
||||||
|
const codeDataKey = codeColumn.data
|
||||||
|
|
||||||
|
// 计算该列的最大长度
|
||||||
const maxLen = data.reduce((m: number, r: any) => {
|
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)
|
return Math.max(m, v.length)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const charWidth = 9
|
const charWidth = 9
|
||||||
const basePadding = 24
|
const basePadding = 24
|
||||||
const nested = hot.getPlugin('nestedRows')
|
const nested = hot.getPlugin('nestedRows')
|
||||||
@@ -84,100 +180,71 @@ export const computeCodeColWidth = (hot: any): number => {
|
|||||||
return Math.min(Math.max(80, width), 480)
|
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 createNode = (dataSchema: any,level: string): any => ({
|
||||||
const getContainerAndIndexByLevel = (data: any[], level: string) => {
|
...dataSchema,
|
||||||
const seg = parseLevel(level)
|
level,
|
||||||
if (seg.length === 0) return null
|
__children: []
|
||||||
if (seg.length === 1) return { container: data, index: seg[0], parentLevel: null }
|
})
|
||||||
|
const getSelectedVisualRowRange = (hot: any): { startRow: number; amount: number } | null => {
|
||||||
let parent = data[seg[0]]
|
const sel = hot.getSelectedLast?.() ?? hot.getSelected?.()?.[0]
|
||||||
for (let i = 1; i < seg.length - 1; i++) {
|
if (!sel) return null
|
||||||
if (!Array.isArray(parent.__children)) parent.__children = []
|
|
||||||
parent = parent.__children[seg[i] - 1]
|
const [r1, , r2] = sel as [number, number, number, number]
|
||||||
if (!parent) return null
|
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') {
|
if (type === 'delete') {
|
||||||
// 删除行
|
const range = getSelectedVisualRowRange(hot)
|
||||||
const loc = getContainerAndIndexByLevel(data, currentLevel)
|
if (!range) return
|
||||||
if (!loc) return
|
hot.alter('remove_row', range.startRow, range.amount, 'remove_row_alter')
|
||||||
const { container, index, parentLevel } = loc
|
return
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hot.updateSettings({ data })
|
const sel = hot.getSelected()
|
||||||
// nestedRowsPlugin.headersUI.updateRowHeaderWidth()
|
if (!sel?.length) return
|
||||||
hot.render()
|
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 type { Ref } from 'vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { DropdownInstance, TreeV2Instance } from 'element-plus'
|
import type { DropdownInstance, TreeV2Instance } from 'element-plus'
|
||||||
|
import { contextMenuManager } from '../db-hst/contextMenuManager'
|
||||||
|
|
||||||
interface NodeBase<T> { id: string; label: string; children?: T[] }
|
interface NodeBase<T> { id: string; label: string; children?: T[] }
|
||||||
type NodeType<T> = T & NodeBase<T>
|
type NodeType<T> = T & NodeBase<T>
|
||||||
@@ -31,11 +32,19 @@ interface LevelConfig {
|
|||||||
addKey?: string
|
addKey?: string
|
||||||
addText?: string
|
addText?: string
|
||||||
allowDelete?: boolean
|
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 {
|
interface HierarchyConfig {
|
||||||
rootKey: string
|
rootKey: string
|
||||||
rootText: string
|
rootText: string
|
||||||
|
onRootAdd?: (newNode: any, allRootNodes: any[]) => void | Promise<void>
|
||||||
|
onRootDelete?: (node: any) => void | Promise<void>
|
||||||
levels: LevelConfig[]
|
levels: LevelConfig[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +82,9 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const depth = getDepth(node, ctx)
|
const depth = getDepth(node, ctx)
|
||||||
|
console.log('getMenuItems - 节点:', node, '深度:', depth)
|
||||||
const levelConfig = this.config.levels.find(l => l.depth === depth)
|
const levelConfig = this.config.levels.find(l => l.depth === depth)
|
||||||
|
console.log('找到的 levelConfig:', levelConfig)
|
||||||
|
|
||||||
if (!levelConfig) {
|
if (!levelConfig) {
|
||||||
// 未配置的层级,只显示删除
|
// 未配置的层级,只显示删除
|
||||||
@@ -82,9 +93,26 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
|||||||
|
|
||||||
const items: MenuItem[] = []
|
const items: MenuItem[] = []
|
||||||
|
|
||||||
// 添加子级菜单项
|
// 添加子级菜单项(检查条件)
|
||||||
if (levelConfig.addKey && levelConfig.addText) {
|
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) {
|
if (!node && cmd === this.config.rootKey) {
|
||||||
const next = ctx.createNode('root')
|
const next = ctx.createNode('root')
|
||||||
next.label = this.config.rootText.replace('添加', '')
|
next.label = this.config.rootText.replace('添加', '')
|
||||||
|
|
||||||
|
// 先添加到数据中
|
||||||
ctx.setData([...ctx.dataRef.value, next])
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,29 +157,108 @@ class HierarchyContextMenuHandler<T> implements ContextMenuHandler<T> {
|
|||||||
if (cmd === 'delete') {
|
if (cmd === 'delete') {
|
||||||
const target = ctx.locate(node.id)
|
const target = ctx.locate(node.id)
|
||||||
if (!target) return
|
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)
|
target.container.splice(target.index, 1)
|
||||||
ctx.setData([...ctx.dataRef.value])
|
ctx.setData([...ctx.dataRef.value])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找匹配的层级配置
|
// 查找匹配的层级配置
|
||||||
const depth = getDepth(node, ctx)
|
const depth = getDepth(node, ctx)
|
||||||
const levelConfig = this.config.levels.find(l => l.depth === depth && l.addKey === cmd)
|
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 next = ctx.createNode(node.id)
|
if (levelConfig) {
|
||||||
next.label = levelConfig.addText?.replace('添加', '') || '新目录'
|
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)
|
const customLevelConfig = this.config.levels.find(l => l.depth === getDepth(node, ctx))
|
||||||
ctx.setData([...ctx.dataRef.value])
|
if (customLevelConfig?.onCustomCommand) {
|
||||||
ctx.expandNode(target.node.id)
|
await customLevelConfig.onCustomCommand(cmd, node)
|
||||||
ctx.setCurrentKey(next.id)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +296,7 @@ class DefaultContextMenuHandler<T> implements ContextMenuHandler<T> {
|
|||||||
target.node.children.push(ctx.createNode(target.node.id))
|
target.node.children.push(ctx.createNode(target.node.id))
|
||||||
ctx.setData([...ctx.dataRef.value])
|
ctx.setData([...ctx.dataRef.value])
|
||||||
ctx.expandNode(target.node.id)
|
ctx.expandNode(target.node.id)
|
||||||
ctx.setCurrentKey(target.node.id)
|
// ctx.setCurrentKey(target.node.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (cmd === 'rename') { ctx.startEdit(node); return }
|
if (cmd === 'rename') { ctx.startEdit(node); return }
|
||||||
@@ -191,10 +317,22 @@ class DbTreeContextMenu<T> {
|
|||||||
|
|
||||||
private config: ContextMenuConfig<T>
|
private config: ContextMenuConfig<T>
|
||||||
private handler: ContextMenuHandler<T>
|
private handler: ContextMenuHandler<T>
|
||||||
|
private unregister: (() => void) | null = null
|
||||||
|
|
||||||
constructor(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T>) {
|
constructor(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T>) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.handler = handler ?? new DefaultContextMenuHandler<T>()
|
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> => {
|
private createNode = (prefix: string): NodeType<T> => {
|
||||||
@@ -223,18 +361,30 @@ class DbTreeContextMenu<T> {
|
|||||||
getMenuItems = (): MenuItem[] => this.handler.getMenuItems(this.currentNode.value, this.ctx())
|
getMenuItems = (): MenuItem[] => this.handler.getMenuItems(this.currentNode.value, this.ctx())
|
||||||
|
|
||||||
openContextMenu = (event: MouseEvent, nodeData: NodeType<T>) => {
|
openContextMenu = (event: MouseEvent, nodeData: NodeType<T>) => {
|
||||||
const { clientX, clientY } = event
|
// console.log('openContextMenu',nodeData)
|
||||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
// 通知管理器即将打开新菜单,关闭其他菜单
|
||||||
|
contextMenuManager.notifyOpening(this.closeContextMenu)
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
const { clientX, clientY } = event
|
||||||
this.currentNode.value = nodeData
|
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()
|
this.dropdownRef.value?.handleOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
openBlankContextMenu = (event: MouseEvent) => {
|
openBlankContextMenu = (event: MouseEvent) => {
|
||||||
const { clientX, clientY } = event
|
// console.log('openBlankContextMenu')
|
||||||
this.position.value = DOMRect.fromRect({ x: clientX, y: clientY })
|
// 通知管理器即将打开新菜单,关闭其他菜单
|
||||||
|
contextMenuManager.notifyOpening(this.closeContextMenu)
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
const { clientX, clientY } = event
|
||||||
this.currentNode.value = null
|
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()
|
this.dropdownRef.value?.handleOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@
|
|||||||
placeholder="请输入关键字"
|
placeholder="请输入关键字"
|
||||||
@input="onQueryChanged"
|
@input="onQueryChanged"
|
||||||
/>
|
/>
|
||||||
|
<!-- @contextmenu="openBlankContextMenu" -->
|
||||||
<ElTreeV2
|
<ElTreeV2
|
||||||
class="treeLine-2"
|
class="treeLine-2"
|
||||||
:indent="0"
|
:indent="0"
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
style="max-width: 600px"
|
|
||||||
:data="data"
|
:data="data"
|
||||||
:props="props"
|
:props="props"
|
||||||
:filter-method="filterMethod"
|
:filter-method="filterMethod"
|
||||||
@@ -19,49 +20,64 @@
|
|||||||
:height="treeHeight"
|
:height="treeHeight"
|
||||||
@node-expand="onNodeExpand"
|
@node-expand="onNodeExpand"
|
||||||
@node-collapse="onNodeCollapse"
|
@node-collapse="onNodeCollapse"
|
||||||
@contextmenu="openBlankContextMenu"
|
@contextmenu="handleContextMenu"
|
||||||
|
@nodeContextmenu="handleNodeContextMenu"
|
||||||
highlight-current
|
highlight-current
|
||||||
scrollbar-always-on
|
scrollbar-always-on
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
@nodeClick="onNodeSingleClick"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data: nodeData }">
|
<template #default="{ node, data: nodeData }">
|
||||||
<!-- 根据层级生成占位符,level 1 不需要占位 -->
|
<!-- 根据层级生成占位符,level 1 不需要占位 -->
|
||||||
|
<!-- :style="{ paddingLeft: node.isLeaf ? '0px' : '0px' }" -->
|
||||||
<span v-for="i in (node.level - 1)" :key="i" class="node_nestingLevel_empty"></span>
|
<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' }">
|
<div class="node-content-wrapper" :style="{ paddingLeft: '5px' }">
|
||||||
<span class="node-icon-wrapper">
|
<div class="node-left-content">
|
||||||
<IconifyIcon
|
<span class="node-icon-wrapper" >
|
||||||
v-if="!node.isLeaf"
|
<IconifyIcon
|
||||||
:icon="node.expanded ? 'ep:remove' : 'ep:circle-plus'"
|
v-if="!node.isLeaf"
|
||||||
class="custom-expand-icon"
|
:icon="node.expanded ? 'ep:remove' : 'ep:circle-plus'"
|
||||||
/>
|
class="custom-expand-icon"
|
||||||
</span>
|
@click.stop="onNodeIconWrapperClick(node)"
|
||||||
<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 }}
|
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTreeV2>
|
</ElTreeV2>
|
||||||
@@ -91,18 +107,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { 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 type { ContextMenuHandler, HierarchyConfig } from './contextMenu'
|
||||||
import { useInlineEdit } from './inlineEdit'
|
import { useContextMenu } from './contextMenu'
|
||||||
import { useDragAndDrop } from './draggable'
|
import { useDragAndDrop } from './draggable'
|
||||||
|
import { useInlineEdit } from './inlineEdit'
|
||||||
defineOptions({ name: 'DbTree' });
|
defineOptions({ name: 'DbTree' });
|
||||||
const componentProps = defineProps<{ height?: number; data?: Tree[]; defaultExpandedKeys?: number | string | string[]; contextMenuHandler?: ContextMenuHandler<Tree> | HierarchyConfig; search?: boolean }>()
|
const componentProps = defineProps<{
|
||||||
const emit = defineEmits<{ (e: 'select', node: Tree): void }>()
|
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 {
|
interface Tree {
|
||||||
id: string
|
id: string
|
||||||
@@ -122,6 +147,14 @@ watch(
|
|||||||
|
|
||||||
const treeHeight = computed(() => componentProps.height ?? 400)
|
const treeHeight = computed(() => componentProps.height ?? 400)
|
||||||
const isSearchEnabled = computed(() => componentProps.search ?? true)
|
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 = {
|
const props = {
|
||||||
value: 'id',
|
value: 'id',
|
||||||
label: 'label',
|
label: 'label',
|
||||||
@@ -173,6 +206,17 @@ const onQueryChanged = (query: string) => {
|
|||||||
const filterMethod = (query: string, node: TreeNodeData) =>
|
const filterMethod = (query: string, node: TreeNodeData) =>
|
||||||
node.label!.includes(query)
|
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 }
|
type LocateResult = { node: Tree; parent: Tree | null; container: Tree[]; index: number }
|
||||||
|
|
||||||
const locate = (id: string): LocateResult | null => {
|
const locate = (id: string): LocateResult | null => {
|
||||||
@@ -187,36 +231,97 @@ const locate = (id: string): LocateResult | null => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
type TreeKey = string | number
|
||||||
const { editingId, editingLabel, editingOriginalLabel, startEdit, saveEdit, cancelEdit } = useInlineEdit<Tree>({ dataRef: data, locate })
|
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 { 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 })
|
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
|
let clickTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const clickDelayMs = 250
|
|
||||||
const triggerSingleClick = (node: Tree) => {
|
|
||||||
closeContextMenu()
|
|
||||||
emit('select', node)
|
|
||||||
}
|
|
||||||
const onNodeSingleClick = (node: Tree, e: MouseEvent) => {
|
const onNodeSingleClick = (node: Tree, e: MouseEvent) => {
|
||||||
|
// 清除之前的定时器
|
||||||
if (clickTimer) {
|
if (clickTimer) {
|
||||||
clearTimeout(clickTimer)
|
clearTimeout(clickTimer)
|
||||||
clickTimer = null
|
clickTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 单击延迟执行,避免与双击冲突
|
||||||
clickTimer = setTimeout(() => {
|
clickTimer = setTimeout(() => {
|
||||||
triggerSingleClick(node)
|
closeContextMenu()
|
||||||
|
emit('select', node)
|
||||||
clickTimer = null
|
clickTimer = null
|
||||||
}, clickDelayMs)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNodeDblClick = (node: Tree, e: MouseEvent) => {
|
const onNodeDblClick = (node: Tree, e: MouseEvent) => {
|
||||||
|
console.log('onNodeDblClick')
|
||||||
|
|
||||||
|
// 清除单击定时器,避免触发单击事件
|
||||||
if (clickTimer) {
|
if (clickTimer) {
|
||||||
clearTimeout(clickTimer)
|
clearTimeout(clickTimer)
|
||||||
clickTimer = null
|
clickTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接启动编辑,不需要防抖
|
||||||
startEdit(node)
|
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 onNodeExpand = (data: TreeNodeData) => {
|
||||||
const key = (data as any)[props.value] as string
|
const key = (data as any)[props.value] as string
|
||||||
if (!key) return
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -244,9 +368,23 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
|||||||
// gap: 8px;
|
// gap: 8px;
|
||||||
}
|
}
|
||||||
.treeLine-2 {
|
.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 {
|
.node_nestingLevel_empty {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-left: 18px;
|
padding-left: 13px;
|
||||||
}
|
}
|
||||||
.node_nestingLevel_empty::before {
|
.node_nestingLevel_empty::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -271,6 +409,16 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0px;
|
gap: 0px;
|
||||||
line-height: 1;
|
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 {
|
.node-icon-wrapper {
|
||||||
@@ -303,19 +451,34 @@ const onNodeCollapse = (data: TreeNodeData) => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 100px;
|
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 的元素(不是最后一个) */
|
/* 选择后面还有 ht_nestingLevel_empty 的元素(不是最后一个) */
|
||||||
.node_nestingLevel_empty:has(+ .node_nestingLevel_empty)::after {
|
.node_nestingLevel_empty:has(+ .node_nestingLevel_empty)::after {
|
||||||
/* 你的样式 */
|
/* 你的样式 */
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
:deep(.el-tree-node){
|
:deep(.el-tree-node){
|
||||||
left: -9px !important;
|
// left: -9px !important;
|
||||||
|
|
||||||
}
|
}
|
||||||
:deep(.el-tree-node__expand-icon){
|
:deep(.el-tree-node__expand-icon){
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
// :deep(.el-tree-node__content){
|
:deep(.el-tree-node .el-tree-node__content){
|
||||||
// display: none !important;
|
// padding-left: 5px !important;
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ type LocateResult<T> = { node: NodeType<T>; parent: NodeType<T> | null; containe
|
|||||||
export const useInlineEdit = <T>(params: {
|
export const useInlineEdit = <T>(params: {
|
||||||
dataRef: Ref<NodeType<T>[]>;
|
dataRef: Ref<NodeType<T>[]>;
|
||||||
locate: (id: string) => LocateResult<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 editingId = ref<string | null>(null)
|
||||||
const editingLabel = ref('')
|
const editingLabel = ref('')
|
||||||
const editingOriginalLabel = ref('')
|
const editingOriginalLabel = ref('')
|
||||||
@@ -24,12 +25,18 @@ export const useInlineEdit = <T>(params: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEdit = () => {
|
const saveEdit = async () => {
|
||||||
if (!editingId.value) return
|
if (!editingId.value) return
|
||||||
const target = locate(editingId.value)
|
const target = locate(editingId.value)
|
||||||
if (!target) { editingId.value = null; return }
|
if (!target) { editingId.value = null; return }
|
||||||
const next = editingLabel.value.trim()
|
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]
|
dataRef.value = [...dataRef.value]
|
||||||
editingId.value = null
|
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[]>([
|
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:'name',title:'名称'},
|
||||||
{type:'text',data:'unit',title:'单位'},
|
{type:'text',data:'unit',title:'单位'},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -233,12 +233,12 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<ElSplitter >
|
<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">
|
<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="categoryHandleSelect" :defaultExpandedKeys="2" :search="false" />
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel collapsible :min="200">
|
<ElSplitterPanel :min="200">
|
||||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
|||||||
const hstRef = ref<any>(null)
|
const hstRef = ref<any>(null)
|
||||||
const columns = ref<any[]>([
|
const columns = ref<any[]>([
|
||||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
{ 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: 'category', title: '类别' },
|
||||||
{ type: 'text', data: 'name', title: '名称' },
|
{ type: 'text', data: 'name', title: '名称' },
|
||||||
{ type: 'text', data: 'feature', title: '项目特征' },
|
{ type: 'text', data: 'feature', title: '项目特征' },
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const dialogRenderer = (instance: any, td: HTMLElement, row: number, col: number
|
|||||||
|
|
||||||
const columns = ref<any[]>([
|
const columns = ref<any[]>([
|
||||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
{ 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: 'spec', title: '费用代码', width: 300,renderer: dialogRenderer },
|
||||||
{ type: 'text', data: 'cardinal', title: '计算基数(用户端不显示)', width: 200 },
|
{ type: 'text', data: 'cardinal', title: '计算基数(用户端不显示)', width: 200 },
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -39,28 +39,107 @@ export const treeData = ref<Tree[]>([
|
|||||||
export const contextMenuHandler = {
|
export const contextMenuHandler = {
|
||||||
rootKey: 'add-category',
|
rootKey: 'add-category',
|
||||||
rootText: '添加定额',
|
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: [
|
levels: [
|
||||||
{
|
{
|
||||||
depth: 0,
|
depth: 0,
|
||||||
addKey: 'add-province',
|
addKey: 'add-province',
|
||||||
addText: '添加省市',
|
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,
|
depth: 1,
|
||||||
addKey: 'add-name',
|
addKey: 'add-name',
|
||||||
addText: '添加名称',
|
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,
|
depth: 2,
|
||||||
addKey: 'add-model',
|
addKey: 'add-model',
|
||||||
addText: '添加模式',
|
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,
|
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">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed } from 'vue'
|
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed } from 'vue'
|
||||||
import { Page } from '@vben/common-ui';
|
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 { useElementSize } from '@vueuse/core'
|
||||||
import { DbTree } from '#/components/db-tree';
|
import { DbTree } from '#/components/db-tree';
|
||||||
import { DbHst } from '#/components/db-hst';
|
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 catalogTreeData, contextMenuHandler as catalogContextMenuHandler } from './catalog';
|
||||||
import { treeData as topTreeData, contextMenuHandler as topContextMenuHandler } from './catalog';
|
import { treeData as topTreeData, contextMenuHandler as topContextMenuHandler } from './catalog';
|
||||||
import { dbSettings as projectDbSettings } from './project';
|
import { dbSettings as projectDbSettings, contextMenuItems as projectContextMenuItems, initProjectHst, load as projectLoad } from './project';
|
||||||
import { dbSettings as subDbSettings } from './sub';
|
import { dbSettings as subDbSettings, contextMenuItems as subContextMenuItems, setEditDescCallback, initSubHst, load as subLoad } from './sub';
|
||||||
import { dbSettings as guideDbSettings } from './guide';
|
import { dbSettings as guideDbSettings, contextMenuItems as guideContextMenuItems, initGuideHst, load as guideLoad } from './guide';
|
||||||
import { dbSettings as bottomDbSettings } from './rightHst';
|
import { dbSettings as bottomDbSettings, contextMenuItems as bottomContextMenuItems, initBottomHst, load as bottomLoad } from './rightHst';
|
||||||
|
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
@@ -29,66 +30,221 @@ const subHstRef = ref<any>(null)
|
|||||||
const guideHstRef = ref<any>(null)
|
const guideHstRef = ref<any>(null)
|
||||||
const bottomHstRef = 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) => {
|
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[]) {
|
function onResizeEnd(index: number, sizes: number[]) {
|
||||||
// 通过 hotInstance 来操作 Handsontable
|
// 通过 hotInstance 来操作 Handsontable
|
||||||
console.log(index,sizes)
|
// console.log(index,sizes)
|
||||||
onTopHeight(sizes[0])
|
onTopHeight(sizes[0])
|
||||||
onBottomHeight(sizes[1])
|
onBottomHeight(sizes[1])
|
||||||
}
|
}
|
||||||
function onTopHeight(height: number){
|
function onTopHeight(height: number){
|
||||||
// if (topHstRef.value?.hotInstance) {
|
topContainerHeight.value = height
|
||||||
// topHstRef.value.hotInstance.updateSettings({
|
|
||||||
// height: height-15,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// topHstRef.value.hotInstance.render()
|
|
||||||
|
|
||||||
// // console.log('onResizeEnd-onTopHeight',height, 'codeColWidth:', topHstRef.value.codeColWidth);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
function onBottomHeight(height: number){
|
function onBottomHeight(height: number){
|
||||||
// if (bottomHstRef.value?.hotInstance) {
|
if (bottomHstRef.value?.hotInstance) {
|
||||||
// bottomHstRef.value.hotInstance.updateSettings({
|
bottomHstRef.value.hotInstance.updateSettings({
|
||||||
// height: height-15
|
height: height
|
||||||
// })
|
})
|
||||||
|
bottomHstRef.value.hotInstance.render()
|
||||||
// // bottomHstRef.value.loadData(bootomMock())
|
}
|
||||||
// // bottomHstRef.value.hotInstance.render()
|
}
|
||||||
// console.log('onResizeEnd-bottomHstRef',height);
|
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(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
|
// 设置编辑回调
|
||||||
|
setEditDescCallback(handleEditDesc)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// onTopHeight(topContainerHeight.value)
|
// onTopHeight(topContainerHeight.value)
|
||||||
// onBottomHeight(bottomContainerHeight.value)
|
// onBottomHeight(bottomContainerHeight.value)
|
||||||
|
|
||||||
if (projectHstRef.value?.hotInstance) {
|
if (projectHstRef.value?.hotInstance) {
|
||||||
|
// 初始化 projectHstRef
|
||||||
|
initProjectHst(projectHstRef, (row: number | null) => {
|
||||||
|
console.log('projectHstRef-选中的行:', row)
|
||||||
|
// 在这里处理点击事件
|
||||||
|
subLoad(generateMockData())
|
||||||
|
})
|
||||||
projectHstRef.value?.hotInstance.updateSettings({
|
projectHstRef.value?.hotInstance.updateSettings({
|
||||||
height: containerHeight.value - 15
|
height: containerHeight.value
|
||||||
})
|
})
|
||||||
// 更新 code 列的宽度
|
// 更新 code 列的宽度
|
||||||
projectHstRef.value.updateCodeColWidth()
|
// projectHstRef.value.updateCodeColWidth()
|
||||||
}
|
}
|
||||||
if (subHstRef.value?.hotInstance) {
|
if (subHstRef.value?.hotInstance) {
|
||||||
|
// 初始化 subHstRef
|
||||||
|
initSubHst(subHstRef, (row: number | null) => {
|
||||||
|
console.log('subHstRef-选中的行:', row)
|
||||||
|
// 在这里处理点击事件
|
||||||
|
guideLoad(generateMockData())
|
||||||
|
})
|
||||||
subHstRef.value?.hotInstance.updateSettings({
|
subHstRef.value?.hotInstance.updateSettings({
|
||||||
height: containerHeight.value - 15
|
height: containerHeight.value
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
if (guideHstRef.value?.hotInstance) {
|
if (guideHstRef.value?.hotInstance) {
|
||||||
|
// 初始化 guideHstRef
|
||||||
|
initGuideHst(guideHstRef, (row: number | null) => {
|
||||||
|
console.log('guideHstRef-选中的行:', row)
|
||||||
|
// 在这里处理点击事件
|
||||||
|
bottomLoad(generateMockData())
|
||||||
|
})
|
||||||
guideHstRef.value?.hotInstance.updateSettings({
|
guideHstRef.value?.hotInstance.updateSettings({
|
||||||
height: containerHeight.value - 15
|
height: containerHeight.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomHstRef.value?.hotInstance) {
|
if (bottomHstRef.value?.hotInstance) {
|
||||||
|
// 初始化 bottomHstRef
|
||||||
|
initBottomHst(bottomHstRef, (row: number | null) => {
|
||||||
|
console.log('bottomHstRef-选中的行:', row)
|
||||||
|
// 在这里处理点击事件
|
||||||
|
})
|
||||||
bottomHstRef.value?.hotInstance.updateSettings({
|
bottomHstRef.value?.hotInstance.updateSettings({
|
||||||
height: containerHeight.value
|
height: bottomContainerHeight.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,41 +258,85 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<ElSplitter >
|
<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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel collapsible :min="200">
|
<ElSplitterPanel :min="200">
|
||||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" >
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel collapsible :min="200">
|
<ElSplitterPanel :min="200">
|
||||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full" >
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</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" >
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel :min="200">
|
||||||
<ElSplitterPanel size="20%">
|
<ElSplitterPanel size="20%">
|
||||||
<ElSplitter layout="vertical" @resize-end="onResizeEnd">
|
<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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
</ElSplitter>
|
</ElSplitter>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
</ElSplitter>
|
</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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,108 @@
|
|||||||
//清单指引
|
//清单指引
|
||||||
import { ref } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
const columns = ref<any[]>([
|
import type { Ref } from 'vue'
|
||||||
{type:'text',data:'code',title:'编码'},
|
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||||
{type:'text',data:'name',title:'名称'},
|
|
||||||
{type:'text',data:'unit',title:'单位',width:30},
|
|
||||||
|
|
||||||
])
|
// 用于存储 guideHstRef 的引用
|
||||||
|
let guideHstRef: Ref<any> | null = null
|
||||||
|
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||||
|
|
||||||
const mockData = ()=>{
|
// 回调函数,供 config.vue 使用
|
||||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
|
||||||
|
// 初始化函数,由 config.vue 调用
|
||||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
export function initGuideHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||||
code: `A${String(index + 1).padStart(6, '0')}`,
|
guideHstRef = hstRef
|
||||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
onCellMouseDownCallback = callback || null
|
||||||
unit: units[index % units.length],
|
|
||||||
}))
|
|
||||||
return mockData;
|
|
||||||
}
|
}
|
||||||
let rowSchema: any = {}
|
|
||||||
// 根据 columns 的 data 字段生成对象结构
|
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||||
columns.value.forEach((col: any) => {
|
getHotInstance: () => guideHstRef?.value?.hotInstance,
|
||||||
if (col.data) {
|
|
||||||
rowSchema[col.data] = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export let dbSettings = {
|
// 包装 load 函数,在加载前重置选中行
|
||||||
data: mockData(),
|
export function load(data: any[]) {
|
||||||
dataSchema: rowSchema,
|
selectedRow.value = null
|
||||||
colWidths: 100,
|
return originalLoad(data)
|
||||||
columns: columns.value
|
|
||||||
}
|
}
|
||||||
|
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 { ref, nextTick } from 'vue'
|
||||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
import type { Ref } from 'vue'
|
||||||
const columns = ref<any[]>([
|
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||||
{type:'text',data:'code',title:'编号',renderer: codeRenderer},
|
|
||||||
{type:'text',data:'name',title:'名称'},
|
// 用于存储 projectHstRef 的引用
|
||||||
{type:'text',data:'unit',title:'单位', width: 50},
|
let projectHstRef: Ref<any> | null = null
|
||||||
])
|
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||||
const mockData = ()=>{
|
|
||||||
const mockData = []
|
// 回调函数,供 config.vue 使用
|
||||||
let codeCounter = 1
|
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||||
|
|
||||||
// 生成5个父级工程项目
|
// 初始化函数,由 config.vue 调用
|
||||||
for (let i = 0; i < 5; i++) {
|
export function initProjectHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||||
const parentCode = `PRJ${String(codeCounter++).padStart(2, '0')}`
|
projectHstRef = hstRef
|
||||||
const parent = {
|
onCellMouseDownCallback = callback || null
|
||||||
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
|
|
||||||
}
|
}
|
||||||
let rowSchema: any = {level: null, __children: []}
|
|
||||||
// 根据 columns 的 data 字段生成对象结构
|
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||||
columns.value.forEach((col: any) => {
|
getHotInstance: () => projectHstRef?.value?.hotInstance,
|
||||||
if (col.data && col.data !== 'level' && col.data !== '__children') {
|
|
||||||
rowSchema[col.data] = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 包装 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 = {
|
export let dbSettings = {
|
||||||
data: mockData(),
|
data: [],
|
||||||
dataSchema: rowSchema,
|
dataSchema: initSchema(columns),
|
||||||
colWidths: 160,
|
colWidths: 160,
|
||||||
columns: columns.value,
|
columns: columns,
|
||||||
rowHeaders: false,
|
rowHeaders: false,
|
||||||
nestedRows: true,
|
nestedRows: false,
|
||||||
bindRowsWithHeaders: true,
|
bindRowsWithHeaders: true,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
items: {
|
items: {
|
||||||
@@ -104,5 +76,37 @@ export let dbSettings = {
|
|||||||
// redo: {}
|
// 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'
|
import { ref, nextTick } from 'vue'
|
||||||
const columns = ref<any[]>([
|
import type { Ref } from 'vue'
|
||||||
{type:'text',data:'code',title:'编码'},
|
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||||
{type:'text',data:'name',title:'名称'},
|
|
||||||
{type:'text',data:'unit',title:'单位',width:30},
|
|
||||||
|
|
||||||
])
|
// 用于存储 bottomHstRef 的引用
|
||||||
|
let bottomHstRef: Ref<any> | null = null
|
||||||
|
const selectedRow = ref<number | null>(null) // 记录当前选中的行
|
||||||
|
|
||||||
const mockData = ()=>{
|
// 回调函数,供 config.vue 使用
|
||||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
|
||||||
|
// 初始化函数,由 config.vue 调用
|
||||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
export function initBottomHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||||
code: `B${String(index + 1).padStart(6, '0')}`,
|
bottomHstRef = hstRef
|
||||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
onCellMouseDownCallback = callback || null
|
||||||
unit: units[index % units.length],
|
|
||||||
}))
|
|
||||||
return mockData;
|
|
||||||
}
|
}
|
||||||
let rowSchema: any = {}
|
|
||||||
// 根据 columns 的 data 字段生成对象结构
|
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||||
columns.value.forEach((col: any) => {
|
getHotInstance: () => bottomHstRef?.value?.hotInstance,
|
||||||
if (col.data) {
|
|
||||||
rowSchema[col.data] = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export let dbSettings = {
|
// 包装 load 函数,在加载前重置选中行
|
||||||
data: mockData(),
|
export function load(data: any[]) {
|
||||||
dataSchema: rowSchema,
|
selectedRow.value = null
|
||||||
colWidths: 100,
|
return originalLoad(data)
|
||||||
columns: columns.value
|
|
||||||
}
|
}
|
||||||
|
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'
|
import { ref, nextTick } from 'vue'
|
||||||
const columns = ref<any[]>([
|
import type { Ref } from 'vue'
|
||||||
{type:'text',data:'code',title:'编码'},
|
import { useParentChildLineNestedRowsFalse } from '#/components/db-hst/nestedRows'
|
||||||
{type:'text',data:'name',title:'名称'},
|
import { selectCellBorderStyle } from '#/components/db-hst/base'
|
||||||
{type:'text',data:'unit',title:'单位',width:30},
|
|
||||||
{type:'text',data:'desc',title:'清单说明'},
|
|
||||||
|
|
||||||
])
|
// 用于存储 subHstRef 的引用
|
||||||
|
let subHstRef: Ref<any> | null = null
|
||||||
|
const selectedRow = ref<any>(null) // 记录当前选中的行
|
||||||
|
|
||||||
const mockData = ()=>{
|
// 回调函数,供 config.vue 使用
|
||||||
const units = ['m³', 'm²', 'm', 'kg', 't', '个', '套', '台', '块', '根']
|
let onCellMouseDownCallback: ((row: number | null) => void) | null = null
|
||||||
const categories = ['混凝土', '钢材', '木材', '砖瓦', '管材', '电缆', '涂料', '五金', '设备', '其他']
|
|
||||||
|
// 初始化函数,由 config.vue 调用
|
||||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
export function initSubHst(hstRef: Ref<any>, callback?: (row: number | null) => void) {
|
||||||
code: `MAT${String(index + 1).padStart(6, '0')}`,
|
subHstRef = hstRef
|
||||||
name: `${categories[index % categories.length]}材料${index + 1}`,
|
onCellMouseDownCallback = callback || null
|
||||||
unit: units[index % units.length],
|
|
||||||
desc: `这是详细说明,`
|
|
||||||
}))
|
|
||||||
return mockData;
|
|
||||||
}
|
}
|
||||||
let rowSchema: any = {}
|
|
||||||
// 根据 columns 的 data 字段生成对象结构
|
const { load: originalLoad, codeRenderer, handleRowOperation, initSchema, highlightDeselect } = useParentChildLineNestedRowsFalse({
|
||||||
columns.value.forEach((col: any) => {
|
getHotInstance: () => subHstRef?.value?.hotInstance,
|
||||||
if (col.data) {
|
|
||||||
rowSchema[col.data] = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export let dbSettings = {
|
|
||||||
data: mockData(),
|
// 包装 load 函数,在加载前重置选中行
|
||||||
dataSchema: rowSchema,
|
export function load(data: any[]) {
|
||||||
colWidths: 100,
|
selectedRow.value = null
|
||||||
columns: columns.value
|
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:'编码'},
|
// 直接使用配置对象,DbTree 会自动创建 HierarchyContextMenuHandler
|
||||||
{type:'text',data:'name',title:'名称'},
|
export const contextMenuHandler = {
|
||||||
{type:'text',data:'category',title:'类别',
|
rootKey: 'add-category',
|
||||||
renderer: 'db-dropdown',
|
rootText: '添加工料机总类',
|
||||||
source: ['人', '人机', '材', '机'],
|
onRootAdd: async (newNode: any, allRootNodes: any[]) => {
|
||||||
readOnly: true,
|
console.log('添加工料机总类:', newNode, '所有根节点:', allRootNodes)
|
||||||
},
|
|
||||||
{type:'text',data:'priceExTax',title:'除税基价代码'},
|
// 计算根节点数量作为新的 sortOrder
|
||||||
{type:'text',data:'priceInTax',title:'含税基价代码'},
|
const newSortOrder = allRootNodes.length + 1
|
||||||
{type:'text',data:'priceExTaxComp',title:'除税编制代码'},
|
newNode.code = 'ROOT'
|
||||||
{type:'text',data:'priceInTaxComp',title:'含税编制代码'},
|
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">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch } from 'vue'
|
import {
|
||||||
import { Page } from '@vben/common-ui';
|
createCategoryTreeMapping,
|
||||||
import { ElSplitter,ElSplitterPanel,ElCard,ElScrollbar } from 'element-plus';
|
deleteCategoryTreeMapping,
|
||||||
import { DbTree } from '#/components/db-tree';
|
getCatalogsTreeList, getCategoriesTree,
|
||||||
|
updateCategoriesTree
|
||||||
|
} from '#/api/database/materials/index';
|
||||||
import { DbHst } from '#/components/db-hst';
|
import { DbHst } from '#/components/db-hst';
|
||||||
import { useElementSize } from '@vueuse/core'
|
|
||||||
// import { materialsColumns } from './category'
|
import { validatorRow } from '#/components/db-hst/validator';
|
||||||
import { manualRowMoveRenderer } from '#/components/db-hst/sort'
|
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 containerRef = ref<HTMLElement | null>(null)
|
||||||
const { height: containerHeight } = useElementSize(containerRef)
|
const { height: containerHeight } = useElementSize(containerRef)
|
||||||
type Tree = { id: string; label: string; children?: Tree[] }
|
type Tree = { id: string; label: string; children?: Tree[] }
|
||||||
|
|
||||||
const treeData = ref<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 dbHstRef = ref<any>(null)
|
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[] = [
|
const materialsColumns:any[] = [
|
||||||
// {type:'text',data:'sort',title:'⇅', renderer: manualRowMoveRenderer,
|
// {type:'text',data:'sort',title:'⇅', renderer: manualRowMoveRenderer,
|
||||||
@@ -83,55 +33,304 @@ const materialsColumns:any[] = [
|
|||||||
// readOnly: true,
|
// readOnly: true,
|
||||||
// editor: false,
|
// editor: false,
|
||||||
// },
|
// },
|
||||||
{type:'text',data:'code',title:'编码'},
|
{type:'text',data:'sortOrder',title:'序号',readOnly: true, allowInvalid: true, className: 'htCenter'},
|
||||||
{type:'text',data:'name',title:'名称'},
|
{type:'text',data:'name',title:'名称',readOnly: true, allowInvalid: true},
|
||||||
{type:'text',data:'category',title:'类别',
|
{type:'text',data:'code',title:'类别', allowInvalid: true,
|
||||||
renderer: 'db-dropdown',
|
renderer: 'db-dropdown',
|
||||||
source: ['人', '人机', '材', '机'],
|
isOnlySelect: true,
|
||||||
readOnly: true,
|
// source 会在 category.vue 中动态设置
|
||||||
},
|
onAfterSelect: (instance: any, row: number, column: number, oldValue: any, newValue: string, optData?: any) => {
|
||||||
{type:'text',data:'priceExTax',title:'除税基价代码'},
|
console.log('选择后回调:', { row, column, oldValue, newValue, optData })
|
||||||
{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++
|
|
||||||
})
|
|
||||||
|
|
||||||
let dbSettings = {
|
if(newValue == oldValue){
|
||||||
columns: materialsColumns,
|
return
|
||||||
data: test,
|
}
|
||||||
// manualRowMove: true,
|
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
|
getCatalogsTreeList().then(res=>{
|
||||||
|
console.log('原始数据:', res)
|
||||||
|
treeData.value = transformTreeData(res)
|
||||||
|
console.log('转换后数据:', treeData.value)
|
||||||
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('containerHeight.value',containerHeight.value)
|
// console.log('containerHeight.value',containerHeight.value)
|
||||||
dbHstRef?.value.hotInstance.updateSettings({
|
dbHstRef?.value.hotInstance.updateSettings({
|
||||||
height: containerHeight.value,
|
height: containerHeight.value,
|
||||||
})
|
})
|
||||||
|
// loadingPlugin.value = dbHstRef?.value.hotInstance.getPlugin('loading');
|
||||||
// 生成模拟数据
|
// loadingPlugin.value.show();
|
||||||
const categories = ['人', '人机', '材', '机']
|
// loadingPlugin.value.hide();
|
||||||
const mockData = Array.from({ length: 300 }, (_, index) => ({
|
// dbHstRef?.value.loadData(mockData)
|
||||||
code: `MAT${String(index + 1).padStart(6, '0')}`,
|
// dbHstRef?.value.hotInstance.render()
|
||||||
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()
|
|
||||||
|
|
||||||
}, 200);
|
}, 200);
|
||||||
})
|
})
|
||||||
onUnmounted(() => {})
|
onUnmounted(() => {})
|
||||||
@@ -142,12 +341,13 @@ onUnmounted(() => {})
|
|||||||
<ElSplitter>
|
<ElSplitter>
|
||||||
<ElSplitterPanel 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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel :min="200">
|
<ElSplitterPanel :min="200">
|
||||||
<ElCard class="w-full h-full" body-class="!p-0 h-full" >
|
<ElCard class="w-full h-full" body-class="!p-0 h-full">
|
||||||
<DbHst ref="dbHstRef" :settings="dbSettings" class=""></DbHst>
|
<DbHst ref="dbHstRef" :settings="dbSettings" :contextMenuItems="contextMenuItems"></DbHst>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
</ElSplitter>
|
</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">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed } from 'vue'
|
import { DbCalc } from '#/components/db-calc'
|
||||||
import { Page } from '@vben/common-ui';
|
import { DbHst } from '#/components/db-hst'
|
||||||
import { ElSplitter,ElSplitterPanel,ElCard } from 'element-plus';
|
import { DbTree } from '#/components/db-tree'
|
||||||
|
import { Page } from '@vben/common-ui'
|
||||||
import { useElementSize } from '@vueuse/core'
|
import { useElementSize } from '@vueuse/core'
|
||||||
import { DbTree } from '#/components/db-tree';
|
import { ElCard, ElSplitter, ElSplitterPanel, ElTableV2 } from 'element-plus'
|
||||||
import { DbHst } from '#/components/db-hst';
|
import { onMounted, ref } from 'vue'
|
||||||
import { topColHeaders } from './machine'
|
import { useBottomTable } from './composables/useBottomTable'
|
||||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
import { useCalcDialog } from './composables/useCalcDialog'
|
||||||
// import { sourceDataObject } from '#/components/db-hst/mockData'
|
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 containerRef = ref<HTMLElement | null>(null)
|
||||||
const { height: containerHeight } = useElementSize(containerRef)
|
const { height: containerHeight } = useElementSize(containerRef)
|
||||||
const topContainerRef = ref<HTMLElement | null>(null)
|
const topContainerRef = ref<HTMLElement | null>(null)
|
||||||
@@ -15,409 +21,209 @@ const { height: topContainerHeight } = useElementSize(topContainerRef)
|
|||||||
const bottomContainerRef = ref<HTMLElement | null>(null)
|
const bottomContainerRef = ref<HTMLElement | null>(null)
|
||||||
const { height: bottomContainerHeight } = useElementSize(bottomContainerRef)
|
const { height: bottomContainerHeight } = useElementSize(bottomContainerRef)
|
||||||
|
|
||||||
type Tree = { id: string; label: string; children?: Tree[] }
|
const mergedId = ref<number>()
|
||||||
const categoryTreeData = ref<Tree[]>([
|
const detailTreeRef = ref<any>()
|
||||||
{
|
|
||||||
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[]>([
|
|
||||||
'编码',
|
|
||||||
'名称',
|
|
||||||
'型号规格',
|
|
||||||
'类别',
|
|
||||||
'单位',
|
|
||||||
'税率',
|
|
||||||
'除税基价',
|
|
||||||
'含税基价',
|
|
||||||
'除税编制价',
|
|
||||||
'含税编制价',
|
|
||||||
'计算基数'
|
|
||||||
])
|
|
||||||
|
|
||||||
// 生成 tableData 模拟数据
|
const {
|
||||||
const generateTableData = () => {
|
categoryTreeData,
|
||||||
const categories = ['人', '人机', '材', '机']
|
catalogsTreeId,
|
||||||
const units = ['台', '个', 't', 'm³', 'm²', 'kg']
|
loadCategoryTree,
|
||||||
|
handleSelect: categoryHandleSelect
|
||||||
return Array.from({ length: 20 }, (_, index) => ({
|
} = useCategoryTree()
|
||||||
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 tableData = ref<any[]>(generateTableData())
|
const {
|
||||||
const bottomColumns = ref<any[]>([
|
detailTreeData,
|
||||||
{type:'text',data:'code',title:'编码'},
|
catalogItemId,
|
||||||
{type:'text',data:'name',title:'名称',
|
catalogsTreeId: detailCatalogsTreeId,
|
||||||
renderer: 'db-table',
|
catalogueMenuHandler,
|
||||||
customDropdownSource: tableData.value,
|
loadDetailTree,
|
||||||
customGetLabel: (item: any) => item?.name,
|
handleSelect: detailHandleSelect,
|
||||||
customTableHeaders: tableHeaders.value,
|
handleEditSave: handleCatalogItemEditSave
|
||||||
// 自定义字段键,按顺序对应表头
|
} = useDetailTree(catalogsTreeId)
|
||||||
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:'除税基价'},
|
|
||||||
|
|
||||||
{type:'text',data:'priceExTax1',title:'除税编制价'},
|
const {
|
||||||
{type:'text',data:'priceExTax2',title:'含税编制价'},
|
bottomHstRef,
|
||||||
{type:'text',data:'priceExTax3',title:'定额消耗量'},
|
bottomDbSettings,
|
||||||
{type:'text',data:'priceExTax4',title:'含税编制合价'},
|
bottomContextMenuItems,
|
||||||
{type:'text',data:'priceExTax5',title:'除税基价合价'},
|
loadData: loadBottomData,
|
||||||
{type:'text',data:'priceExTax6',title:'含税基价合价'},
|
updateHeight: updateBottomHeight,
|
||||||
{type:'text',data:'priceExTax7',title:'除税编制合价'},
|
updateNameColumn
|
||||||
{type:'text',data:'calcBase',title:'计算基数(隐藏)'},
|
} = useBottomTable(mergedId)
|
||||||
|
|
||||||
])
|
const {
|
||||||
// const colHeaders = ref<string[]>(topColHeaders)
|
topHstRef,
|
||||||
const topHstRef = ref<any>(null)
|
topColumns,
|
||||||
const bottomHstRef = ref<any>(null)
|
topDbSettings,
|
||||||
let rowSchema: any = {level: null,__children: []}
|
topContextMenuItems,
|
||||||
|
calcTableData,
|
||||||
// 根据 columns 的 data 字段生成对象结构
|
loadData: loadTopData,
|
||||||
topColumns.value.forEach((col: any) => {
|
loadAllowedCategories,
|
||||||
if (col.data && col.data !== 'level' && col.data !== '__children') {
|
loadTopDataClear
|
||||||
rowSchema[col.data] = null
|
} = 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 = ()=>{
|
updateNameColumn(selectRender, itemCallbacks)
|
||||||
// 生成带层级结构的模拟数据
|
|
||||||
const categories = ['人', '人机', '材', '机']
|
const handleCategorySelect = async (node: any) => {
|
||||||
const units = ['台', '个', 't', 'm³', 'm²', 'kg']
|
console.log(node)
|
||||||
|
await categoryHandleSelect(node)
|
||||||
const mockData = []
|
// 同步更新 detailTree 的 catalogsTreeId
|
||||||
let codeCounter = 1
|
detailCatalogsTreeId.value = node.id
|
||||||
|
await loadDetailTree(node.id)
|
||||||
// 生成5个父级项目
|
await loadTopDataClear()
|
||||||
for (let i = 0; i < 5; i++) {
|
detailTreeRef?.value.setCurrentKey(null)
|
||||||
const parentCode = `MAC${String(codeCounter++).padStart(6, '0')}`
|
}
|
||||||
const parent = {
|
|
||||||
code: parentCode,
|
const handleDetailSelect = async (node: any) => {
|
||||||
name: `机械设备分类${i + 1}`,
|
const id = detailHandleSelect(node)
|
||||||
spec: `规格${i + 1}`,
|
if (id) {
|
||||||
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: []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每个父级生成2-4个子项
|
await loadAllowedCategories()
|
||||||
const childCount = Math.floor(Math.random() * 3) + 2
|
await loadTopData(id)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mockData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let topDbSettings = {
|
const onResizeEnd = (_index: number, sizes: number[]) => {
|
||||||
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)
|
|
||||||
onTopHeight(sizes[0])
|
onTopHeight(sizes[0])
|
||||||
onBottomHeight(sizes[1])
|
onBottomHeight(sizes[1])
|
||||||
}
|
}
|
||||||
function onTopHeight(height: number){
|
|
||||||
|
const onTopHeight = (height: number) => {
|
||||||
if (topHstRef.value?.hotInstance) {
|
if (topHstRef.value?.hotInstance) {
|
||||||
topHstRef.value.hotInstance.updateSettings({
|
topHstRef.value.hotInstance.updateSettings({ height: height - 8 })
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onBottomHeight = (height: number) => {
|
||||||
|
updateBottomHeight(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadCategoryTree()
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onTopHeight(topContainerHeight.value)
|
onTopHeight(topContainerHeight.value)
|
||||||
onBottomHeight(bottomContainerHeight.value)
|
onBottomHeight(bottomContainerHeight.value)
|
||||||
}, 200);
|
}, 200)
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<ElSplitter >
|
<ElSplitter>
|
||||||
<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" ref="containerRef">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel >
|
|
||||||
|
<ElSplitterPanel :min="200">
|
||||||
<ElSplitter layout="vertical" @resize-end="onResizeEnd">
|
<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">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
<ElSplitterPanel collapsible :min="200">
|
|
||||||
|
<ElSplitterPanel :min="200">
|
||||||
<ElCard class="w-full h-full" body-class="!p-0 h-full" ref="bottomContainerRef">
|
<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>
|
</ElCard>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
</ElSplitter>
|
</ElSplitter>
|
||||||
</ElSplitterPanel>
|
</ElSplitterPanel>
|
||||||
</ElSplitter>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
|
|
||||||
</style>
|
</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)
|
console.log('MarketMaterials newHeight', newHeight)
|
||||||
if (newHeight && hstRef.value?.hotInstance) {
|
if (newHeight && hstRef.value?.hotInstance) {
|
||||||
hstRef.value.hotInstance.updateSettings({
|
hstRef.value.hotInstance.updateSettings({
|
||||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||||
})
|
})
|
||||||
hstRef.value.hotInstance.render()
|
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">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { getAdjustmentCombinedList } from '#/api/database/quota';
|
||||||
import { DbHst } from '#/components/db-hst'
|
import { DbHst } from '#/components/db-hst';
|
||||||
|
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
height?: number
|
height?: number
|
||||||
|
quotaItemId?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const topHstRef = ref<any>(null)
|
const topHstRef = ref<any>(null)
|
||||||
const bottomHstRef = ref<any>(null)
|
|
||||||
|
|
||||||
// 第一个表格:调整、附注名称、附注内容
|
// 第一个表格:序号、名称、调整
|
||||||
const topColumns = ref<any[]>([
|
const topColumns = ref<any[]>([
|
||||||
{ type: 'checkbox', data: 'adjustment', title: '调整' },
|
{
|
||||||
{ type: 'text', data: 'noteName', title: '附注名称' },
|
type: 'text',
|
||||||
{ type: 'text', data: 'noteContent', title: '附注内容' }
|
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 = () => {
|
const topSettings = ref({
|
||||||
return Array.from({ length: 10 }, (_, index) => ({
|
data: [],
|
||||||
adjustment: index % 2 === 0, // checkbox 类型使用 boolean 值
|
|
||||||
noteName: `附注${index + 1}`,
|
|
||||||
noteContent: `附注内容说明${index + 1}`
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const topSettings = {
|
|
||||||
data: topMockData(),
|
|
||||||
columns: topColumns.value,
|
columns: topColumns.value,
|
||||||
colWidths: 150,
|
colWidths: 150,
|
||||||
height: 135
|
height: 135,
|
||||||
}
|
readOnly: false, // 改为可编辑,因为调整字段需要编辑
|
||||||
|
})
|
||||||
|
|
||||||
// 第二个表格:名称、定额值、实际值
|
// 加载调整设置与明细的组合列表数据
|
||||||
const bottomColumns = ref<any[]>([
|
const loadAdjustmentDetails = async () => {
|
||||||
{ type: 'text', data: 'name', title: '名称' },
|
if (!props.quotaItemId) {
|
||||||
{ type: 'text', data: 'quotaValue', title: '定额值' },
|
console.warn('quotaItemId is required')
|
||||||
{ type: 'text', data: 'actualValue', title: '实际值' }
|
// 清空表格
|
||||||
])
|
if (topHstRef.value?.hotInstance) {
|
||||||
|
topHstRef.value.hotInstance.loadData([])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const bottomMockData = () => {
|
try {
|
||||||
return Array.from({ length: 10 }, (_, index) => ({
|
console.log('开始加载调整明细列表, quotaItemId:', props.quotaItemId)
|
||||||
name: `项目${index + 1}`,
|
const response = await getAdjustmentCombinedList(props.quotaItemId)
|
||||||
quotaValue: (Math.random() * 100 + 10).toFixed(2),
|
console.log('接口返回数据:', response)
|
||||||
actualValue: (Math.random() * 100 + 10).toFixed(2)
|
|
||||||
}))
|
// 组合列表返回的是调整设置数组,每个设置包含其明细
|
||||||
}
|
// 需要将其展平为表格行数据
|
||||||
|
const flattenedData: any[] = []
|
||||||
const bottomSettings = {
|
|
||||||
data: bottomMockData(),
|
if (response && Array.isArray(response)) {
|
||||||
columns: bottomColumns.value,
|
response.forEach((setting: any) => {
|
||||||
colWidths: 150,
|
console.log('处理调整设置:', setting)
|
||||||
height: 135
|
|
||||||
|
// 从 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(
|
watch(
|
||||||
@@ -58,41 +154,50 @@ watch(
|
|||||||
(newHeight) => {
|
(newHeight) => {
|
||||||
console.log('QuotaAdjustment newHeight', newHeight)
|
console.log('QuotaAdjustment newHeight', newHeight)
|
||||||
if (newHeight) {
|
if (newHeight) {
|
||||||
const tableHeight = (newHeight - 50 - 15) / 2 // 平均分配给两个表格
|
const tableHeight = newHeight - 50
|
||||||
|
nextTick(() => {
|
||||||
if (topHstRef.value?.hotInstance) {
|
if (topHstRef.value?.hotInstance) {
|
||||||
topHstRef.value.hotInstance.updateSettings({
|
topHstRef.value.hotInstance.updateSettings({
|
||||||
height: tableHeight
|
height: tableHeight
|
||||||
})
|
})
|
||||||
topHstRef.value.hotInstance.render()
|
topHstRef.value.hotInstance.render()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if (bottomHstRef.value?.hotInstance) {
|
|
||||||
bottomHstRef.value.hotInstance.updateSettings({
|
|
||||||
height: tableHeight
|
|
||||||
})
|
|
||||||
bottomHstRef.value.hotInstance.render()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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(() => {
|
onMounted(() => {
|
||||||
console.log('QuotaAdjustment mounted')
|
console.log('QuotaAdjustment mounted, quotaItemId:', props.quotaItemId)
|
||||||
|
// onMounted 时调用一次
|
||||||
|
if (props.quotaItemId) {
|
||||||
|
loadAdjustmentDetails()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ topHstRef, bottomHstRef })
|
defineExpose({ topHstRef, loadAdjustmentDetails })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex flex-col gap-2">
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
<div class="flex-1">
|
<DbHst ref="topHstRef" :settings="topSettings" />
|
||||||
<DbHst ref="topHstRef" :settings="topSettings" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<DbHst ref="bottomHstRef" :settings="bottomSettings" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
import { createAdjustmentSetting, deleteAdjustmentSetting, getAdjustmentSettingList, updateAdjustmentSetting } from '#/api/database/quota/index';
|
||||||
import { ElDialog,ElTabPane, ElTable ,ElTableColumn,ElInput,ElButton} from 'element-plus';
|
import { DbHst } from '#/components/db-hst';
|
||||||
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<{
|
const props = defineProps<{
|
||||||
height?: number
|
height?: number
|
||||||
|
quotaItemId?: number | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hstRef = ref<any>(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 dialogVisible = ref(false)
|
||||||
const activeTab = ref('tab1')
|
const activeTab = ref('tab1Settings')
|
||||||
const currentCell = reactive({ row: 0, col: 0 })
|
const currentCell = reactive({ row: 0, col: 0 })
|
||||||
// Tab 标签映射
|
// Tab 标签映射
|
||||||
const tabLabels: Record<string, string> = {
|
const tabLabels: Record<string, string> = {
|
||||||
'tab1': '调整工料机系数',
|
'tab1Columns': '调整人材机消耗量',
|
||||||
'tab2': '动态合并定额',
|
'tab2Columns': '动态合并定额',
|
||||||
'tab3': '增减耗量',
|
'tab3Columns': '增减材料消耗量',
|
||||||
'tab4': '动态调整人材机消耗量'
|
'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 dbSettings = ref<any>({
|
||||||
const coefficientData = ref([
|
data: [],
|
||||||
{ category: '人工', coefficient: 2, selected: false },
|
columns: [],
|
||||||
{ category: '材料', coefficient: 1.5, selected: false },
|
colWidths: 150,
|
||||||
{ category: '机具', coefficient: 1.5, selected: false },
|
height: 270
|
||||||
{ category: '其他', coefficient: 2, selected: false }
|
})
|
||||||
])
|
|
||||||
|
|
||||||
// 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 = () => {
|
const handleClose = () => {
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (hstRef?.value.hotInstance) {
|
if (!topHstRef.value?.hotInstance || !hstRef.value?.hotInstance) {
|
||||||
const tabLabel = tabLabels[activeTab.value]
|
ElMessage.error('表格实例未初始化')
|
||||||
hstRef?.value.hotInstance.setDataAtCell(currentCell.row, currentCell.col, tabLabel)
|
return
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRow = (tabName: string) => {
|
try {
|
||||||
switch(tabName) {
|
// 获取弹窗内表格的数据
|
||||||
case 'tab2':
|
const dialogData = topHstRef.value.hotInstance.getData()
|
||||||
quotaMergeData.value.push({ code: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' })
|
|
||||||
break
|
// 过滤掉空行
|
||||||
case 'tab3':
|
const validData = dialogData.filter((row: any) => {
|
||||||
consumptionData.value.push({ code: '', name: '', value: '' })
|
return row && row.some((cell: any) => cell !== null && cell !== '')
|
||||||
break
|
})
|
||||||
case 'tab4':
|
|
||||||
dynamicAdjustData.value.push({ name: '', category: '', coefficient: '', minValue: '', maxValue: '', denominatorValue: '', baseValue: '', rule: '' })
|
if (validData.length === 0) {
|
||||||
break
|
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[]>([
|
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: 'name', title: '名称' },
|
||||||
{ type: 'text', data: 'quotaValue', title: '定额值' },
|
{ type: 'text', data: 'adjustType', title: '调整类型', renderer: 'db-dropdown', width: 200,
|
||||||
{ type: 'text', data: 'adjustContent', title: '调整内容' },
|
source: typeLists,
|
||||||
{ type: 'text', data: 'adjustType', title: '调整类型', renderer: tabDialog, width: 200},
|
onAfterSelect: (_instance: any, row: number, column: number, oldValue: any, newValue: string, _optData?: any) => {
|
||||||
{ type: 'text', data: 'adjustRule', title: '调整规则' }
|
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 = {
|
const settings = {
|
||||||
data: mockData(),
|
data: [], // 初始为空,等待从接口加载
|
||||||
columns: columns.value,
|
columns: columns.value,
|
||||||
colWidths: 120,
|
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(
|
watch(
|
||||||
() => props.height,
|
() => props.height,
|
||||||
(newHeight) => {
|
(newHeight) => {
|
||||||
console.log('QuotaAdjustmentQuery newHeight', newHeight)
|
console.log('QuotaAdjustmentQuery newHeight', newHeight)
|
||||||
|
nextTick(() => {
|
||||||
if (newHeight && hstRef.value?.hotInstance) {
|
if (newHeight && hstRef.value?.hotInstance) {
|
||||||
hstRef.value.hotInstance.updateSettings({
|
hstRef.value.hotInstance.updateSettings({
|
||||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||||
})
|
})
|
||||||
hstRef.value.hotInstance.render()
|
hstRef.value.hotInstance.render()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
@@ -154,18 +540,20 @@ defineExpose({ hstRef })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full" @contextmenu.prevent>
|
||||||
<DbHst ref="hstRef" :settings="settings" />
|
<DbHst ref="hstRef" :settings="settings" :contextMenuItems="contextMenuItems"/>
|
||||||
|
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
title="调数类型"
|
:title="tabLabels[activeTab] || '调整设置'"
|
||||||
width="50%"
|
width="60%"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
|
:destroy-on-close="true"
|
||||||
>
|
>
|
||||||
<ElTabs v-model="activeTab">
|
<DbHst v-if="dialogVisible" ref="topHstRef" :settings="dbSettings" :contextMenuItems="dialogContextMenuItems"></DbHst>
|
||||||
<!-- Tab1: 调整工料机系数 -->
|
<!-- <ElTabs v-model="activeTab" :tab-position="'top'">
|
||||||
<ElTabPane label="调整工料机系数" name="tab1">
|
|
||||||
|
<ElTabPane v-if="activeTab === 'tab1'" label="调整工料机系数" name="tab1">
|
||||||
<ElTable :data="coefficientData" border style="width: 50%">
|
<ElTable :data="coefficientData" border style="width: 50%">
|
||||||
<ElTableColumn type="selection" width="55" />
|
<ElTableColumn type="selection" width="55" />
|
||||||
<ElTableColumn prop="category" label="类别" width="200" />
|
<ElTableColumn prop="category" label="类别" width="200" />
|
||||||
@@ -177,8 +565,7 @@ defineExpose({ hstRef })
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|
||||||
<!-- Tab2: 动态合并定额 -->
|
<ElTabPane v-if="activeTab === 'tab2'" label="动态合并定额" name="tab2">
|
||||||
<ElTabPane label="动态合并定额" name="tab2">
|
|
||||||
<ElTable :data="quotaMergeData" border style="width: 100%">
|
<ElTable :data="quotaMergeData" border style="width: 100%">
|
||||||
<ElTableColumn prop="code" label="编码" width="120">
|
<ElTableColumn prop="code" label="编码" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -214,8 +601,8 @@ defineExpose({ hstRef })
|
|||||||
<ElButton @click="addRow('tab2')" style="margin-top: 10px">添加行</ElButton>
|
<ElButton @click="addRow('tab2')" style="margin-top: 10px">添加行</ElButton>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|
||||||
<!-- Tab3: 增减耗量 -->
|
|
||||||
<ElTabPane label="增减耗量" name="tab3">
|
<ElTabPane v-if="activeTab === 'tab3'" label="增减耗量" name="tab3">
|
||||||
<ElTable :data="consumptionData" border style="width: 100%">
|
<ElTable :data="consumptionData" border style="width: 100%">
|
||||||
<ElTableColumn prop="code" label="编码" width="200">
|
<ElTableColumn prop="code" label="编码" width="200">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -236,8 +623,8 @@ defineExpose({ hstRef })
|
|||||||
<ElButton @click="addRow('tab3')" style="margin-top: 10px">添加行</ElButton>
|
<ElButton @click="addRow('tab3')" style="margin-top: 10px">添加行</ElButton>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|
||||||
<!-- Tab4: 动态调整人材机消耗量 -->
|
|
||||||
<ElTabPane label="动态调整人材机消耗量" name="tab4">
|
<ElTabPane v-if="activeTab === 'tab4'" label="动态调整人材机消耗量" name="tab4">
|
||||||
<ElTable :data="dynamicAdjustData" border style="width: 100%">
|
<ElTable :data="dynamicAdjustData" border style="width: 100%">
|
||||||
<ElTableColumn prop="name" label="名称" width="120">
|
<ElTableColumn prop="name" label="名称" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -286,7 +673,7 @@ defineExpose({ hstRef })
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
<ElButton @click="addRow('tab4')" style="margin-top: 10px">添加行</ElButton>
|
<ElButton @click="addRow('tab4')" style="margin-top: 10px">添加行</ElButton>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
</ElTabs>
|
</ElTabs> -->
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
@@ -296,7 +683,7 @@ defineExpose({ hstRef })
|
|||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,53 +1,508 @@
|
|||||||
<script setup lang="ts">
|
<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 { 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<{
|
const props = defineProps<{
|
||||||
height?: number
|
height?: number
|
||||||
|
quotaItemId?: number | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hstRef = ref<any>(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[]>([
|
const onClickOutside = () => {
|
||||||
{ type: 'text', data: 'code', title: '编码' },
|
popoverComponentRef.value?.popoverRef?.hide()
|
||||||
{ type: 'text', data: 'name', title: '名称' },
|
}
|
||||||
{ type: 'text', data: 'spec', title: '型号规格' },
|
// 使用 popover 点击外部关闭的 Hook
|
||||||
{ type: 'text', data: 'unit', title: '单位' },
|
usePopoverClickOutside(visible, onClickOutside)
|
||||||
{ type: 'text', data: 'category', title: '类别' },
|
|
||||||
{ type: 'text', data: 'taxRate', title: '税率' },
|
// 分页状态管理
|
||||||
{ type: 'text', data: 'basePriceExTax', title: '除税基价' },
|
const popoverPagination = reactive({
|
||||||
{ type: 'text', data: 'basePriceInTax', title: '含税基价' },
|
pageNo: 1,
|
||||||
{ type: 'text', data: 'compilePriceExTax', title: '除税编制价' },
|
pageSize: 20,
|
||||||
{ type: 'text', data: 'compilePriceInTax', title: '含税编制价' },
|
total: 0,
|
||||||
{ type: 'text', data: 'quotaQuantity', title: '定额消耗量' },
|
loading: false,
|
||||||
{ type: 'text', data: 'adjustQuantity', title: '调整消耗量' }
|
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 columns = ref<any[]>([
|
||||||
const categories = ['人', '人机', '材', '机']
|
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer, allowInvalid: true, autoWidth: true },
|
||||||
const units = ['工日', 't', 'm³', 'kg', 'm²']
|
{
|
||||||
return Array.from({ length: 20 }, (_, index) => ({
|
type: 'text',
|
||||||
code: `SUB${String(index + 1).padStart(6, '0')}`,
|
data: 'name',
|
||||||
name: `子目项${index + 1}`,
|
title: '名称',
|
||||||
spec: `规格${index + 1}`,
|
readOnly: true,
|
||||||
unit: units[index % units.length],
|
renderer: selectRender,
|
||||||
category: categories[index % categories.length],
|
focusCallback: async (row: any, _col: any, _prop: any, _value: any, rowData: any, instance: any, _td: any) => {
|
||||||
taxRate: `${(Math.random() * 5 + 8).toFixed(2)}%`,
|
// 获得焦点时加载第一页数据
|
||||||
basePriceExTax: (Math.random() * 1000 + 100).toFixed(2),
|
popoverPagination.pageNo = 1
|
||||||
basePriceInTax: (Math.random() * 1100 + 110).toFixed(2),
|
popoverPagination.hasMore = true
|
||||||
compilePriceExTax: (Math.random() * 1200 + 120).toFixed(2),
|
await getTableDataList({}, false)
|
||||||
compilePriceInTax: (Math.random() * 1300 + 130).toFixed(2),
|
console.log('focusCallback-popoverTableData 已加载:', popoverTableData.value)
|
||||||
quotaQuantity: (Math.random() * 10 + 1).toFixed(3),
|
itemSelectPopover.value = { row, currentRowData: rowData, instance }
|
||||||
adjustQuantity: (Math.random() * 10 + 1).toFixed(3)
|
},
|
||||||
}))
|
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 = {
|
const settings = {
|
||||||
data: mockData(),
|
data: [], // 初始为空,等待从接口加载
|
||||||
|
dataSchema: rowSchema,
|
||||||
columns: columns.value,
|
columns: columns.value,
|
||||||
colWidths: 120,
|
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(
|
watch(
|
||||||
() => props.height,
|
() => props.height,
|
||||||
@@ -55,7 +510,7 @@ watch(
|
|||||||
console.log('SubItemMaterials newHeight', newHeight)
|
console.log('SubItemMaterials newHeight', newHeight)
|
||||||
if (newHeight && hstRef.value?.hotInstance) {
|
if (newHeight && hstRef.value?.hotInstance) {
|
||||||
hstRef.value.hotInstance.updateSettings({
|
hstRef.value.hotInstance.updateSettings({
|
||||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
height: newHeight - 50// 减去 tabs 头部和 padding 的高度,滚动条
|
||||||
})
|
})
|
||||||
hstRef.value.hotInstance.render()
|
hstRef.value.hotInstance.render()
|
||||||
}
|
}
|
||||||
@@ -64,17 +519,29 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full" @contextmenu.prevent>
|
||||||
<DbHst ref="hstRef" :settings="settings" />
|
<DbHst ref="hstRef" :settings="settings" :contextMenuItems="contextMenuItems" />
|
||||||
</div>
|
</div>
|
||||||
|
<DbHstPopover
|
||||||
|
ref="popoverComponentRef"
|
||||||
|
:visible="visible"
|
||||||
|
:button-ref="buttonRef"
|
||||||
|
:columns="popoverColumns"
|
||||||
|
:data="popoverTableData"
|
||||||
|
:row-event-handlers="rowEventHandlers"
|
||||||
|
@end-reached="handleTableEndReached"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 { defineConfig } from '@vben/vite-config';
|
||||||
|
|
||||||
|
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||||
import ElementPlus from 'unplugin-element-plus/vite';
|
import ElementPlus from 'unplugin-element-plus/vite';
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
export default defineConfig(async () => {
|
||||||
@@ -10,6 +11,9 @@ export default defineConfig(async () => {
|
|||||||
ElementPlus({
|
ElementPlus({
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
}),
|
}),
|
||||||
|
codeInspectorPlugin({
|
||||||
|
bundler: 'vite',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
"@vitejs/plugin-vue-jsx": "catalog:",
|
"@vitejs/plugin-vue-jsx": "catalog:",
|
||||||
"@vue/test-utils": "catalog:",
|
"@vue/test-utils": "catalog:",
|
||||||
"autoprefixer": "catalog:",
|
"autoprefixer": "catalog:",
|
||||||
|
"code-inspector-plugin": "^1.3.4",
|
||||||
"cross-env": "catalog:",
|
"cross-env": "catalog:",
|
||||||
"cspell": "catalog:",
|
"cspell": "catalog:",
|
||||||
"happy-dom": "catalog:",
|
"happy-dom": "catalog:",
|
||||||
|
|||||||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
@@ -656,6 +656,9 @@ importers:
|
|||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 10.4.22(postcss@8.5.6)
|
version: 10.4.22(postcss@8.5.6)
|
||||||
|
code-inspector-plugin:
|
||||||
|
specifier: ^1.3.4
|
||||||
|
version: 1.3.4
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
@@ -2689,6 +2692,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==}
|
resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@codemirror/autocomplete@6.20.0':
|
||||||
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
|
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
|
||||||
|
|
||||||
@@ -5496,6 +5517,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==}
|
resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
chalk@4.1.1:
|
||||||
|
resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -5614,6 +5639,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
code-inspector-plugin@1.3.4:
|
||||||
|
resolution: {integrity: sha512-735rAAc655oSAMd/6+PIsjpgB5jwugDISom9WFwhNUbOuFHiL2PYwshMmfIhAtOgECl+7g6o50rBIIYwCEa8xg==}
|
||||||
|
|
||||||
codemirror@5.65.20:
|
codemirror@5.65.20:
|
||||||
resolution: {integrity: sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==}
|
resolution: {integrity: sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==}
|
||||||
|
|
||||||
@@ -7788,6 +7816,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==}
|
resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
launch-ide@1.3.1:
|
||||||
|
resolution: {integrity: sha512-opTthrpkuhi1Y8yFn6TWUeycyiI1aiZpVuTV4HQFUfVut7nMYGr5nQ8heYHrRJH2KYISLVYwz+QFRNZxFlbQmA==}
|
||||||
|
|
||||||
lazystream@1.0.1:
|
lazystream@1.0.1:
|
||||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||||
engines: {node: '>= 0.6.3'}
|
engines: {node: '>= 0.6.3'}
|
||||||
@@ -8724,6 +8755,10 @@ packages:
|
|||||||
popmotion@11.0.5:
|
popmotion@11.0.5:
|
||||||
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
|
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:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -12059,6 +12094,48 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
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':
|
'@codemirror/autocomplete@6.20.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.11.3
|
'@codemirror/language': 6.11.3
|
||||||
@@ -15179,6 +15256,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
|
|
||||||
|
chalk@4.1.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
@@ -15336,6 +15418,18 @@ snapshots:
|
|||||||
|
|
||||||
cluster-key-slot@1.1.2: {}
|
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@5.65.20: {}
|
||||||
|
|
||||||
codemirror@6.65.7: {}
|
codemirror@6.65.7: {}
|
||||||
@@ -17678,6 +17772,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
package-json: 10.0.1
|
package-json: 10.0.1
|
||||||
|
|
||||||
|
launch-ide@1.3.1:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
dotenv: 16.6.1
|
||||||
|
|
||||||
lazystream@1.0.1:
|
lazystream@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
@@ -18672,6 +18771,13 @@ snapshots:
|
|||||||
style-value-types: 5.1.2
|
style-value-types: 5.1.2
|
||||||
tslib: 2.4.0
|
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: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss-antd-fixes@0.2.0(postcss@8.5.6):
|
postcss-antd-fixes@0.2.0(postcss@8.5.6):
|
||||||
|
|||||||
Reference in New Issue
Block a user