diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index 77c13d3..7fc6645 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -4,7 +4,9 @@ VITE_PORT=5777 VITE_BASE=/ # 请求路径 -VITE_BASE_URL=http://127.0.0.1:48080 +#VITE_BASE_URL=http://127.0.0.1:48080 +VITE_BASE_URL=http://test.yihuiyong.cn:48080 + # 接口地址 VITE_GLOB_API_URL=/admin-api # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 diff --git a/apps/web-ele/src/api/database/materials/index.ts b/apps/web-ele/src/api/database/materials/index.ts new file mode 100644 index 0000000..66596c0 --- /dev/null +++ b/apps/web-ele/src/api/database/materials/index.ts @@ -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('/core/resource/catalogs'); +// } + +//【第二层】工料机机类树 +export function getCatalogsTreeList() { + return requestClient.get('/core/resource/category-tree/tree'); +} +export function getCategoriesTree(id: any) { + return requestClient.get(`/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(`/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('/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(`/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>( + '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>( + '/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}`); +} diff --git a/apps/web-ele/src/api/database/materials/root.ts b/apps/web-ele/src/api/database/materials/root.ts new file mode 100644 index 0000000..96f97a7 --- /dev/null +++ b/apps/web-ele/src/api/database/materials/root.ts @@ -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('/core/resource/categories'); +} +//5.3 创建类别 +export function createCategories(data: any) { + return requestClient.post(`/core/resource/categories`,data); +} +//5.4 更新类别 +export function updateCategories(data: MaterialsApi.Root) { + return requestClient.put(`/core/resource/categories`,data); +} +//5.5 删除类别 +export function deleteCategories(id: number) { + return requestClient.delete(`/core/resource/categories/${id}`); +} diff --git a/apps/web-ele/src/api/database/quota/fee.ts b/apps/web-ele/src/api/database/quota/fee.ts new file mode 100644 index 0000000..1989d57 --- /dev/null +++ b/apps/web-ele/src/api/database/quota/fee.ts @@ -0,0 +1,22 @@ + +import { requestClient } from '#/api/request'; + +/** 3.8 查看分部分项工程费列表 */ +export function getQuotaFeeItemListWithRate(id: any) { + return requestClient.get(`/core/quota/fee/item/list-with-rate?catalogItemId=${id}`); +} + +/** 获取工料机类别字典(含价格代码) */ +export function getCategoriesByCatalogItem(catalogItemId: number | string) { + return requestClient.get(`/core/quota/catalog-item/${catalogItemId}/categories`); +} + +/** 更新取费项 */ +export function updateQuotaFeeItem(data: any) { + return requestClient.put('/core/quota/fee/item/update', data); +} + +/** 创建取费项 */ +export function createQuotaFeeItem(data: any) { + return requestClient.post('/core/quota/fee/item/create', data); +} \ No newline at end of file diff --git a/apps/web-ele/src/api/database/quota/index.ts b/apps/web-ele/src/api/database/quota/index.ts new file mode 100644 index 0000000..bb3a6a8 --- /dev/null +++ b/apps/web-ele/src/api/database/quota/index.ts @@ -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( + `/core/quota/catalog-item/get?id=${id}` + ); +} + +/** 1.2 获取定额专业树结构 */ +export function getQuotaCatalogItemTree(params?: { exclude?: string }) { + return requestClient.get( + '/core/quota/catalog-item/tree', + { params } + ); +} + +/** 1.5 获取定额专业节点列表 */ +export function getQuotaCatalogItemList() { + return requestClient.get( + '/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( + `/core/quota/catalog-tree/get?id=${id}` + ); +} + +/** 2.5 获取定额子目树节点列表 */ +export function getQuotaCatalogTreeList(catalogItemId: number | string) { + return requestClient.get( + '/core/quota/catalog-tree/list', + { params: { catalogItemId } } + ); +} + +/** 2.6 获取定额子目树结构 */ +export function getQuotaCatalogTree(catalogItemId: number | string) { + return requestClient.get( + '/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('/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( + '/core/quota/catalog-tree/list-by-rate-mode-node', + { params: { rateModeNodeId } } + ); +} + +// ==================== 定额子目管理(第四层) ==================== + +/** 2.4 获取定额子目详情 */ +export function getQuotaItem(id: number) { + return requestClient.get( + `/core/quota/item/get?id=${id}` + ); +} + +/** 2.5 获取定额子目列表 */ +export function getQuotaItemList(catalogItemId: number | string) { + return requestClient.get( + '/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( + '/core/quota/resource/list', + { params: { quotaItemId } } + ); +} + +/** 3.5 获取可选的工料机列表(已过滤范围,支持模糊查询) */ +export function getAvailableResourceList(quotaItemId: number, params?: { code?: string; name?: string; spec?: string }) { + return requestClient.get( + '/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(`/core/quota/adjustment-setting/get?id=${id}`); +} + +/** 5.5 获取调整设置列表 */ +export function getAdjustmentSettingList(quotaItemId: number) { + return requestClient.get( + '/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(`/core/quota/adjustment-detail/get?id=${id}`); +} + +/** 6.5 获取调整明细列表 */ +export function getAdjustmentDetailList(quotaItemId: number) { + return requestClient.get( + '/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( + '/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( + '/core/quota/rate/item/create-mode-node', + data + ); +} +/** 3.8 查看分部分项工程费列表 */ +export function getQuotaRateItemTree(id: any) { + return requestClient.get(`/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(`/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; + }>; + increments?: Array<{ + seq: number; + step: number; + baseThreshold: number; + fieldIncrements: Record; + }>; +}) { + return requestClient.post('/core/quota/rate/item/config-value-rules', data); +} + +/** 7.7 计算动态字段值 */ +export function calculateDynamicFields(rateItemId: number | string, baseValue: number) { + return requestClient.get>( + '/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; +}) { + return requestClient.post('/core/quota/rate/field/save', data); +} + +/** 8.2 更新字段绑定 */ +export function updateFieldBinding( + catalogItemId: number | string, + fieldIndex: number, + bindingIds: Array +) { + return requestClient.post('/core/quota/rate/field/update-binding', bindingIds, { + params: { catalogItemId, fieldIndex } + }); +} + +/** 8.3 获取费率字段列表 */ +export function getRateFieldList(catalogItemId: number | string) { + return requestClient.get( + '/core/quota/rate/field/list', + { params: { catalogItemId } } + ); +} + +// ==================== 字段标签字典管理 ==================== + +/** 9.1 创建字段标签 */ +export function createFieldLabel(data: { + catalogItemId: number | string; + labelName: string; +}) { + return requestClient.post('/core/quota/rate-field-label/create', data); +} + +/** 9.2 更新字段标签 */ +export function updateFieldLabel(data: { + id: number | string; + catalogItemId: number | string; + labelName: string; +}) { + return requestClient.put('/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>('/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 } + }); +} diff --git a/apps/web-ele/src/components/db-calc/index.ts b/apps/web-ele/src/components/db-calc/index.ts new file mode 100644 index 0000000..c380ba0 --- /dev/null +++ b/apps/web-ele/src/components/db-calc/index.ts @@ -0,0 +1 @@ +export { default as DbCalc } from './index.vue'; diff --git a/apps/web-ele/src/components/db-calc/index.vue b/apps/web-ele/src/components/db-calc/index.vue new file mode 100644 index 0000000..c2a8eab --- /dev/null +++ b/apps/web-ele/src/components/db-calc/index.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/apps/web-ele/src/components/db-hst/README.md b/apps/web-ele/src/components/db-hst/README.md new file mode 100644 index 0000000..0e3d9df --- /dev/null +++ b/apps/web-ele/src/components/db-hst/README.md @@ -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` 配置与自定义菜单会自动互斥 diff --git a/apps/web-ele/src/components/db-hst/base.ts b/apps/web-ele/src/components/db-hst/base.ts new file mode 100644 index 0000000..72f47bd --- /dev/null +++ b/apps/web-ele/src/components/db-hst/base.ts @@ -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 +} \ No newline at end of file diff --git a/apps/web-ele/src/components/db-hst/contextMenuManager.ts b/apps/web-ele/src/components/db-hst/contextMenuManager.ts new file mode 100644 index 0000000..2fa2e2a --- /dev/null +++ b/apps/web-ele/src/components/db-hst/contextMenuManager.ts @@ -0,0 +1,56 @@ +/** + * 全局右键菜单管理器 + * 用于管理多个右键菜单实例,确保同一时间只有一个菜单处于打开状态 + */ + +type MenuCloseCallback = () => void + +class ContextMenuManager { + private activeMenus: Set = 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() diff --git a/apps/web-ele/src/components/db-hst/contextmenu.vue b/apps/web-ele/src/components/db-hst/contextmenu.vue new file mode 100644 index 0000000..ba4b830 --- /dev/null +++ b/apps/web-ele/src/components/db-hst/contextmenu.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/apps/web-ele/src/components/db-hst/dropdown.ts b/apps/web-ele/src/components/db-hst/dropdown.ts index e2a3b17..78e8a34 100644 --- a/apps/web-ele/src/components/db-hst/dropdown.ts +++ b/apps/web-ele/src/components/db-hst/dropdown.ts @@ -13,22 +13,38 @@ const closeDropdown = () => { const openDropdown = ( td: HTMLTableCellElement, value: unknown, - source: string[], + source: any[], onSelect: (opt: string) => void, isOptionDisabled?: (opt: string) => boolean, + onAfterSelect?: (oldValue: unknown, newValue: string, optData?: any) => void, ) => { closeDropdown() const menu = document.createElement('div') menu.className = 'ht-dropdown-menu' const frag = document.createDocumentFragment() + for (const opt of source) { + // 支持对象格式 {value, label} 和字符串格式 + const optValue = typeof opt === 'object' && opt !== null ? opt.value : opt + const optLabel = typeof opt === 'object' && opt !== null ? opt.label : opt + const item = document.createElement('div') item.className = 'ht-dropdown-item' - item.textContent = opt - if (String(value) === String(opt)) item.classList.add('is-selected') - const disabled = isDisabled.value && isOptionDisabled?.(opt) === true + item.textContent = optLabel ?? '' + if (String(value) === String(optValue)) item.classList.add('is-selected') + const disabled = isDisabled.value && isOptionDisabled?.(String(optValue)) === true if (disabled) { item.classList.add('is-disabled'); item.setAttribute('aria-disabled', 'true') } - item.onclick = (e) => { e.stopPropagation(); if (disabled) return; onSelect(opt); closeDropdown() } + item.onclick = (e) => { + e.stopPropagation(); + if (disabled) return; + onSelect(String(optValue)); + // 调用选择后的回调,传递旧值、新值和完整数据 + if (onAfterSelect) { + const optData = typeof opt === 'object' && opt !== null ? opt.data : undefined + onAfterSelect(value, String(optValue), optData) + } + closeDropdown() + } frag.appendChild(item) } menu.appendChild(frag) @@ -51,23 +67,71 @@ const openDropdown = ( currentOnDocClick = (ev: MouseEvent) => { const target = ev.target as Node; if (currentDropdownEl && !currentDropdownEl.contains(target)) closeDropdown() } document.addEventListener('click', currentOnDocClick, true) } + export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: any) { td.innerHTML = '' + + // 检查是否需要设置验证背景色 + const cellMeta = instance.getCellMeta(row, column) + const isValid = cellMeta?.valid !== false + + // 如果单元格被标记为无效,设置红色背景 + if (!isValid) { + td.style.backgroundColor = '#ffbeba' // 淡红色背景 + } else { + td.style.backgroundColor = '' // 清除背景色 + } + const wrapper = document.createElement('div') wrapper.className = 'ht-cell-dropdown' const valueEl = document.createElement('span') valueEl.className = 'ht-cell-value' - valueEl.textContent = value ?? '' + + // 基于列配置启用"唯一选择"禁用逻辑 + isDisabled.value = Boolean(cellProperties?.isOnlySelect) + + //TODO 暂时性,后面要删除 + // 如果 isOnlySelect 为 true,设置当前单元格为只读 + const isReadOnly = isDisabled.value && value !== null && value !== undefined && String(value).trim() !== '' + if (isReadOnly) { + instance.setCellMeta(row, column, 'readOnly', true) + // 添加只读样式 + wrapper.classList.add('is-readonly') + td.style.cursor = 'not-allowed' + td.style.opacity = '0.6' + } else { + instance.setCellMeta(row, column, 'readOnly', false) + td.style.cursor = 'pointer' + td.style.opacity = '1' + } + + const source: any[] = Array.isArray(cellProperties?.source) + ? cellProperties.source + : Array.isArray(cellProperties?.customDropdownSource) + ? cellProperties.customDropdownSource + : [] + + // 根据 value 查找对应的 label 显示 + let displayText = value ?? '' + if (value !== null && value !== undefined) { + const matchedOption = source.find(opt => { + const optValue = typeof opt === 'object' && opt !== null ? opt.value : opt + return String(optValue) === String(value) + }) + if (matchedOption) { + displayText = typeof matchedOption === 'object' && matchedOption !== null + ? matchedOption.label + : matchedOption + } + } + + valueEl.textContent = displayText const caretEl = document.createElement('span') caretEl.className = 'ht-cell-caret' wrapper.appendChild(valueEl) wrapper.appendChild(caretEl) td.appendChild(wrapper) - const source: string[] = Array.isArray(cellProperties?.source) - ? cellProperties.source - : Array.isArray(cellProperties?.customDropdownSource) - ? cellProperties.customDropdownSource - : [] + let disabledSet = new Set() if (isDisabled.value) { const colValues = instance.getSourceDataAtCol(column) as unknown[] @@ -75,6 +139,32 @@ export function handlerDropdownRenderer(instance: any, td: HTMLTableCellElement, disabledSet = new Set((Array.isArray(colValues) ? colValues : []).map(v => String(v))) disabledSet.delete(currentStr) } - wrapper.onclick = (e) => { e.stopPropagation(); openDropdown(td, value, source, (opt) => instance.setDataAtCell(row, column, opt), (opt) => isDisabled.value && disabledSet.has(String(opt))) } + + //在 dropdown 的点击事件中阻止冒泡(推荐) + wrapper.onclick = (e) => { + e.stopPropagation() + e.preventDefault() + //TODO 暂时性,后面要删除 + // 如果是只读状态,不打开下拉框 + if (isReadOnly) { + return + } + + const onAfterSelect = cellProperties?.onAfterSelect + openDropdown( + td, + value, + source, + (opt) => instance.setDataAtCell(row, column, opt), + (opt) => isDisabled.value && disabledSet.has(String(opt)), + onAfterSelect ? (oldValue, newValue, optData) => onAfterSelect(instance, row, column, oldValue, newValue, optData) : undefined + ) + } + + // 阻止 mousedown 事件冒泡,防止触发 beforeOnCellMouseDown + wrapper.onmousedown = (e) => { + e.stopPropagation() + } + return td } \ No newline at end of file diff --git a/apps/web-ele/src/components/db-hst/index.vue b/apps/web-ele/src/components/db-hst/index.vue index c3f7f9a..3670ef0 100644 --- a/apps/web-ele/src/components/db-hst/index.vue +++ b/apps/web-ele/src/components/db-hst/index.vue @@ -6,16 +6,28 @@ import { HotTable } from '@handsontable/vue3' import { registerLanguageDictionary, zhCN } from 'handsontable/i18n' import { registerAllModules } from 'handsontable/registry' import 'handsontable/styles/handsontable.css' -import 'handsontable/styles/ht-theme-main.css' +// import 'handsontable/styles/ht-theme-main.css' +import 'handsontable/styles/ht-theme-classic.css'; registerAllModules() registerLanguageDictionary(zhCN) import { handlerDropdownRenderer } from './dropdown' import { handlerTableRenderer } from './table' +import { handlerDuplicateCodeRenderer } from './text' import { computeCodeColWidth,codeRenderer } from './tree' +import ContextMenu from './contextmenu.vue' +import { handleRowOperation } from '#/components/db-hst/tree' // import { sourceDataObject } from './mockData' // const language = ref('zh-CN') defineOptions({ name: 'DbHst' }); -const componentProps = defineProps<{ settings?: any }>() +const componentProps = defineProps<{ + settings?: any + contextMenuItems?: Array<{ + key: string + name: string + callback?: (hotInstance: any) => void + separator?: boolean + }> +}>() // 导入和注册插件和单元格类型 // import { registerCellType, NumericCellType } from 'handsontable/cellTypes'; // import { registerPlugin, UndoRedo } from 'handsontable/plugins'; @@ -24,13 +36,15 @@ const componentProps = defineProps<{ settings?: any }>() // const tableHeight = computed(() => componentProps.height ?? 0) // const defaultData = ref(Array.from({ length: 30 }, () => Array(componentProps.columns?.length ?? 0).fill(''))) const hotTableComponent = ref(null) - +const selectedRow = ref(null) // 记录当前选中的行 const codeColWidth = ref(120) // const colHeaders = ref([]) let defaultSettings = { - themeName: 'ht-theme-main', + // themeName: 'ht-theme-main', + themeName: 'ht-theme-classic', + language: 'zh-CN', // data: sourceDataObject, // colWidths: [100, 120, 100, 100, 100, 100], @@ -40,7 +54,7 @@ let defaultSettings = { // return (index + 1) * 40; // }, // colWidths: undefined, - rowHeights: '23px', // 固定行高 + rowHeights: 23, // 固定行高 wordWrap: false,// 禁止单元格内容自动换行 //manualColumnMove: true, @@ -90,25 +104,69 @@ let defaultSettings = { // return cellProperties; // }, - modifyColWidth: (width: number, col: number) => { - const hot = hotInstance.value - if (!hot) return width - const codeCol = hot.propToCol('code') - // console.log('modifyColWidth',codeCol,width) - return col === codeCol ? (codeColWidth.value ?? width) : width - }, - afterChange: (changes: any, source: string) => { - if (!changes || !hotInstance.value) return - if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return - const hot = hotInstance.value - const codeCol = hot.propToCol('code') - const hasCodeEdit = changes.some((c: any) => c && (c[1] === 'code' || c[1] === codeCol)) - // console.log('afterChange',changes,hasCodeEdit, codeCol) - if (!hasCodeEdit) return - codeColWidth.value = computeCodeColWidth(hot) - // console.log('afterChange',codeColWidth.value) - hot.render() - // console.log('afterChange',codeColWidth.value) + // afterSelection(row1: number, _col1: number, _row2: number, _col2: number) { + // const hot = this as any + // if (selectedRow.value !== null && selectedRow.value !== row1) { + // const colCount = hot.countCols() + // for (let c = 0; c < colCount; c++) { + // const meta = hot.getCellMeta(selectedRow.value, c) + // const classes = (meta.className || '').split(' ').filter(Boolean) + // const idx = classes.indexOf('row-highlight') + // if (idx !== -1) classes.splice(idx, 1) + // hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' ')) + // } + // } + + // selectedRow.value = row1 + + // const colCount = hot.countCols() + // for (let c = 0; c < colCount; c++) { + // const meta = hot.getCellMeta(row1, c) + // const classes = (meta.className || '').split(' ').filter(Boolean) + // if (!classes.includes('row-highlight')) classes.push('row-highlight') + // hot.setCellMeta(row1, c, 'className', classes.join(' ')) + // } + + // hot.render() + // }, + //afterDeselect() { + // const hot = this as any + // if (selectedRow.value === null) return + // const colCount = hot.countCols() + // for (let c = 0; c < colCount; c++) { + // const meta = hot.getCellMeta(selectedRow.value, c) + // const classes = (meta.className || '').split(' ').filter(Boolean) + // const idx = classes.indexOf('row-highlight') + // if (idx !== -1) classes.splice(idx, 1) + // hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' ')) + // } + // selectedRow.value = null + // hot.render() + //}, + + // modifyColWidth(width: number, col: number) { + // const hot = hotInstance.value + // if (!hot) return width + // const codeCol = hot.propToCol('code') + // // console.log('modifyColWidth',codeCol,width) + // return col === codeCol ? (codeColWidth.value ?? width) : width + // }, + // afterChange(changes: any, source: string) { + // if (!changes || !hotInstance.value) return + // if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return + // const hot = hotInstance.value + // const codeCol = hot.propToCol('code') + // const hasCodeEdit = changes.some((c: any) => c && (c[1] === 'code' || c[1] === codeCol)) + // // console.log('afterChange',changes,hasCodeEdit, codeCol) + // if (!hasCodeEdit) return + // codeColWidth.value = computeCodeColWidth(hot) + // // console.log('afterChange',codeColWidth.value) + // hot.render() + // // console.log('afterChange',codeColWidth.value) + // }, + afterOnCellContextMenu: () => { + // Handsontable 内置右键菜单打开后,关闭自定义菜单 + contextMenuRef.value?.hideContextMenu?.() }, } // 合并外部 settings 和默认配置 @@ -116,6 +174,12 @@ let hotSettings = {} // 保留必要的回调函数 const hotInstance = ref(null) +const contextMenuRef = ref(null) + +// 处理右键菜单事件 +const handleContextMenu = (event: MouseEvent) => { + contextMenuRef.value?.handleContextMenu(event) +} onMounted(() => { hotInstance.value = hotTableComponent.value?.hotInstance @@ -127,15 +191,17 @@ watch( () => componentProps.settings, (newSettings) => { if (!newSettings) return + const merged = { ...defaultSettings, ...newSettings, } Object.assign(hotSettings, merged) hotSettings = merged + // console.log(merged) }, - { immediate: true } + { immediate: true,deep:true } ) const loadData = (rows: any[][]) => { @@ -143,12 +209,51 @@ const loadData = (rows: any[][]) => { if (!hotInstance.value) return // hotInstance.value.loadData(rows.length === 0?defaultData.value:rows) hotInstance.value.loadData(rows) - console.log('Source Data:', hotInstance.value.getSourceData()); + //console.log('Source Data:', hotInstance.value.getSourceData()); } const updateCodeColWidth = () => { if (!hotInstance.value) return - codeColWidth.value = computeCodeColWidth(hotInstance.value) + const newWidth = computeCodeColWidth(hotInstance.value) + + // 如果宽度没有变化,不需要更新 + if (newWidth === codeColWidth.value) return + + codeColWidth.value = newWidth + + // 查找配置了 code: true 的列 + const currentSettings = hotInstance.value.getSettings() + const columns = currentSettings.columns || [] + const codeColIndex = columns.findIndex((col: any) => col.code === true) + + if (codeColIndex !== null && codeColIndex >= 0) { + // 获取当前列数 + const colCount = hotInstance.value.countCols() + const currentColWidths = currentSettings.colWidths + + // 构建新的列宽数组 + const newColWidths: number[] = [] + for (let i = 0; i < colCount; i++) { + if (i === codeColIndex) { + newColWidths[i] = codeColWidth.value + } else { + // 获取其他列的当前宽度 + if (Array.isArray(currentColWidths)) { + newColWidths[i] = currentColWidths[i] || 100 + } else if (typeof currentColWidths === 'function') { + newColWidths[i] = 100 + } else { + newColWidths[i] = currentColWidths || 100 + } + } + } + console.log(newColWidths) + // 更新列宽 + hotInstance.value.updateSettings({ + colWidths: newColWidths + }) + } + hotInstance.value.render() } defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, codeColWidth }) @@ -156,11 +261,15 @@ defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, cod Handsontable.renderers.registerRenderer("db-table", handlerTableRenderer); Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer); - +Handsontable.renderers.registerRenderer("db-duplicate", handlerDuplicateCodeRenderer); diff --git a/apps/web-ele/src/components/db-tree/inlineEdit.ts b/apps/web-ele/src/components/db-tree/inlineEdit.ts index ab8d650..e380f25 100644 --- a/apps/web-ele/src/components/db-tree/inlineEdit.ts +++ b/apps/web-ele/src/components/db-tree/inlineEdit.ts @@ -7,8 +7,9 @@ type LocateResult = { node: NodeType; parent: NodeType | null; containe export const useInlineEdit = (params: { dataRef: Ref[]>; locate: (id: string) => LocateResult; + onSave?: (node: NodeType, oldLabel: string, newLabel: string) => void | Promise; }) => { - const { dataRef, locate } = params + const { dataRef, locate, onSave } = params const editingId = ref(null) const editingLabel = ref('') const editingOriginalLabel = ref('') @@ -24,12 +25,18 @@ export const useInlineEdit = (params: { }) } - const saveEdit = () => { + const saveEdit = async () => { if (!editingId.value) return const target = locate(editingId.value) if (!target) { editingId.value = null; return } const next = editingLabel.value.trim() - if (next) target.node.label = next + const oldLabel = editingOriginalLabel.value + if (next && next !== oldLabel) { + target.node.label = next + if (onSave) { + await onSave(target.node, oldLabel, next) + } + } dataRef.value = [...dataRef.value] editingId.value = null } diff --git a/apps/web-ele/src/router/routes/modules/database.ts b/apps/web-ele/src/router/routes/modules/database.ts new file mode 100644 index 0000000..8b4419d --- /dev/null +++ b/apps/web-ele/src/router/routes/modules/database.ts @@ -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; diff --git a/apps/web-ele/src/views/database/entry/add.ts b/apps/web-ele/src/views/database/entry/add.ts new file mode 100644 index 0000000..a1546f9 --- /dev/null +++ b/apps/web-ele/src/views/database/entry/add.ts @@ -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 = ''; + 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 = ''; + 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 = '' + + 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() + }) + } + }, +] \ No newline at end of file diff --git a/apps/web-ele/src/views/database/entry/add.vue b/apps/web-ele/src/views/database/entry/add.vue new file mode 100644 index 0000000..e7ddcaa --- /dev/null +++ b/apps/web-ele/src/views/database/entry/add.vue @@ -0,0 +1,638 @@ + + + + + diff --git a/apps/web-ele/src/views/database/entry/components/AddDialog.vue b/apps/web-ele/src/views/database/entry/components/AddDialog.vue new file mode 100644 index 0000000..99219ad --- /dev/null +++ b/apps/web-ele/src/views/database/entry/components/AddDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/apps/web-ele/src/views/database/entry/components/AttachmentDialog.vue b/apps/web-ele/src/views/database/entry/components/AttachmentDialog.vue new file mode 100644 index 0000000..f9b066f --- /dev/null +++ b/apps/web-ele/src/views/database/entry/components/AttachmentDialog.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/apps/web-ele/src/views/database/entry/components/CopyDialog.vue b/apps/web-ele/src/views/database/entry/components/CopyDialog.vue new file mode 100644 index 0000000..a806b2c --- /dev/null +++ b/apps/web-ele/src/views/database/entry/components/CopyDialog.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/apps/web-ele/src/views/database/entry/components/HistoryDialog.vue b/apps/web-ele/src/views/database/entry/components/HistoryDialog.vue new file mode 100644 index 0000000..af6cf80 --- /dev/null +++ b/apps/web-ele/src/views/database/entry/components/HistoryDialog.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/apps/web-ele/src/views/database/entry/index.ts b/apps/web-ele/src/views/database/entry/index.ts new file mode 100644 index 0000000..02cd4f2 --- /dev/null +++ b/apps/web-ele/src/views/database/entry/index.ts @@ -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 = ` + + + + + + + ` + 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 = ` + + + + + + ` + 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() + }) + } + }, +] \ No newline at end of file diff --git a/apps/web-ele/src/views/database/entry/index.vue b/apps/web-ele/src/views/database/entry/index.vue index 12f1911..9b1efb6 100644 --- a/apps/web-ele/src/views/database/entry/index.vue +++ b/apps/web-ele/src/views/database/entry/index.vue @@ -1,64 +1,208 @@ @@ -643,338 +576,85 @@ onUnmounted(() => { + - + + + + + + + + + + + - + - - -
- - - -
- -
- 拖拽文件至此,或者 - - 选择文件 - -
-
- 已支持 pg/png 每个文件不超过 - 1 MB。 -
-
-
-
- - -
- - - -
- -
- - -
+ + - -
- -
- 价格时间段: - -
- - - - - - - - - - - - -
- - -
+ - - - - - - - - - - - - - + - -
- - - - - - - - - - - - - - - - -
- - -
+ - - -
- - - -
- -
-
- - - -
- - - - - - - - - -
-
-
-
- - -
- - - -
- - - - - - - - - - - -
- - -
+
@@ -1018,35 +698,6 @@ onUnmounted(() => { min-height: 400px; } - .attachment-container { - padding: 10px 0; - } - - .el-form { - padding: 20px 20px 0; - } - - .copy-container { - padding: 10px 0; - } - - .history-container { - padding: 10px 0; - } - - .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; - } - .more-price-container { padding: 10px 0; } diff --git a/apps/web-ele/src/views/database/feature/index.vue b/apps/web-ele/src/views/database/feature/index.vue index c853a2e..e6525ea 100644 --- a/apps/web-ele/src/views/database/feature/index.vue +++ b/apps/web-ele/src/views/database/feature/index.vue @@ -49,7 +49,7 @@ const categoryTreeData = ref([ } ]) const topColumns = ref([ -{type:'text',data:'code',title:'编码', renderer: codeRenderer}, +{type:'text',data:'code',title:'编码', renderer: codeRenderer, code:true}, {type:'text',data:'name',title:'名称'}, {type:'text',data:'unit',title:'单位'}, ]) diff --git a/apps/web-ele/src/views/database/interface/project.vue b/apps/web-ele/src/views/database/interface/project.vue index 14a7d34..b7faab4 100644 --- a/apps/web-ele/src/views/database/interface/project.vue +++ b/apps/web-ele/src/views/database/interface/project.vue @@ -233,12 +233,12 @@ onUnmounted(() => {