第二阶段代码
This commit is contained in:
173
apps/web-ele/src/views/database/interface/cardinalRate.vue
Normal file
173
apps/web-ele/src/views/database/interface/cardinalRate.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { getQuotaCatalogItemTree, updateQuotaCatalogItem } from '#/api/database/quota/index';
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElCard, ElSplitter, ElSplitterPanel, ElTabs, ElTabPane } from 'element-plus';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { init as directoryInit, settings as directorySettings, contextMenuItems as directoryContextMenuItems } from './cardinalRate/directory';
|
||||
import { init as itemInit, settings as itemSettings, contextMenuItems as itemContextMenuItems } from './cardinalRate/item';
|
||||
import { getCatalogTree, updateCatalogNode, getDirectoryTree, getRateItemList } from '#/api/database/interface/cardinalRate';
|
||||
import { rootMenus, nodeMenus } from './cardinalRate/tree';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
|
||||
type Tree = { id: string; label: string; children?: Tree[];[key: string]: any }
|
||||
const treeData = ref<Tree[]>([])
|
||||
|
||||
const directoryHstRef = ref()
|
||||
const itemHstRef = ref()
|
||||
|
||||
// 转换后端数据为树形结构
|
||||
const transformTreeData = (data: any[]): Tree[] => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn('transformTreeData: 数据不是数组', data)
|
||||
return []
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
const treeNode: Tree = {
|
||||
...item, // 保留所有原始属性
|
||||
id: String(item.id),
|
||||
label: item.name || item.label || '未命名',
|
||||
}
|
||||
|
||||
// 只有当 children 存在且长度大于 0 时才递归处理
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
treeNode.children = transformTreeData(item.children)
|
||||
}
|
||||
|
||||
return treeNode
|
||||
})
|
||||
}
|
||||
|
||||
// 转换数据为 Handsontable 格式
|
||||
const transformData = (data: any[]): any[] => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn('transformData: 数据不是数组', data)
|
||||
return []
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
const transformed = {
|
||||
...item, // 保留所有原始属性
|
||||
}
|
||||
|
||||
// 如果有 __children,转换为 children
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
transformed.__children = transformData(item.children)
|
||||
}
|
||||
|
||||
return transformed
|
||||
})
|
||||
}
|
||||
|
||||
// 加载定额专业树(第一层)
|
||||
const loadCategoryTree = async () => {
|
||||
try {
|
||||
const res = await getQuotaCatalogItemTree({ exclude: 'rate_mode,fields_majors' })
|
||||
console.log('定额专业树原始数据:', res)
|
||||
treeData.value = transformTreeData(res)
|
||||
console.log('定额专业树转换后数据:', treeData.value)
|
||||
} catch (error) {
|
||||
console.error('加载定额专业树失败:', error)
|
||||
}
|
||||
// try {
|
||||
// const res = await getCatalogTree()
|
||||
// console.log('定额专业树原始数据:', res)
|
||||
// treeData.value = transformTreeData(res)
|
||||
// console.log('定额专业树转换后数据:', treeData.value)
|
||||
// } catch (error) {
|
||||
// console.error('加载定额专业树失败:', error)
|
||||
// }
|
||||
}
|
||||
const handleEditSave = async (payload: { node: any, data: any, oldLabel: string, newLabel: string }) => {
|
||||
const { node, data, oldLabel, newLabel } = payload
|
||||
console.log('节点编辑保存:', node, data)
|
||||
// 更新节点名称回调
|
||||
await updateQuotaCatalogItem({
|
||||
id: data.id,
|
||||
code: data.code,
|
||||
name: newLabel,
|
||||
});
|
||||
ElMessage.success('更新成功');
|
||||
}
|
||||
// 处理树节点点击(只有 specialty 节点才显示变量设置)
|
||||
const handleNodeClick = async (payload: { node: any, data: any, event: MouseEvent }) => {
|
||||
const { node, data } = payload
|
||||
console.log('节点点击:', node, data)
|
||||
// 清空之前的选择
|
||||
directoryHstRef.value.hotInstance.deselectCell()
|
||||
itemHstRef.value.hotInstance.deselectCell()
|
||||
|
||||
const result = await getDirectoryTree(data.id)
|
||||
directoryHstRef.value.calcBaseRateCatalogId = data.id
|
||||
directoryHstRef.value.nestedRowsLoadData(transformData(result))
|
||||
itemHstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategoryTree()
|
||||
setTimeout(() => {
|
||||
directoryInit(directoryHstRef)
|
||||
itemInit(itemHstRef)
|
||||
|
||||
directoryHstRef.value.loadList = async ()=>{
|
||||
const result = await getDirectoryTree(directoryHstRef.value.calcBaseRateCatalogId)
|
||||
directoryHstRef.value.nestedRowsLoadData(transformData(result))
|
||||
}
|
||||
directoryHstRef.value.onCellMouseDownCallback = async (data: any)=>{
|
||||
console.log('onCellMouseDownCallback',data)
|
||||
if(data.id){
|
||||
itemHstRef.value.calcBaseRateDirectoryId = data.id
|
||||
const result = await getRateItemList(data.id)
|
||||
itemHstRef.value.hotInstance.loadData(result)
|
||||
}
|
||||
}
|
||||
|
||||
itemHstRef.value.loadList = async ()=>{
|
||||
const result = await getRateItemList(itemHstRef.value.calcBaseRateDirectoryId)
|
||||
itemHstRef.value.hotInstance.loadData(result)
|
||||
}
|
||||
|
||||
}, 200);
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<DbTree
|
||||
ref="treeRef"
|
||||
:tree-data="treeData"
|
||||
:draggable="false"
|
||||
:default-expanded-level="4"
|
||||
:root-menus="rootMenus"
|
||||
:node-menus="nodeMenus"
|
||||
@node-edit="handleEditSave"
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel size="25%" :min="200">
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="directoryHstRef" :settings="directorySettings" :contextMenuItems="directoryContextMenuItems"></DbHst>
|
||||
</div>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="itemHstRef" :settings="itemSettings" :contextMenuItems="itemContextMenuItems"></DbHst>
|
||||
</div>
|
||||
</ElSplitterPanel>
|
||||
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { createDirectoryNode, updateDirectoryNode, deleteDirectoryNode } from '#/api/database/interface/cardinalRate';
|
||||
import { validatorRow } from '#/components/db-hst/validator';
|
||||
|
||||
const targetId = ref();
|
||||
let hstRef = ref();
|
||||
const selectedRow = ref()
|
||||
export function init(_hstRef: Ref<any>){
|
||||
hstRef = _hstRef
|
||||
}
|
||||
const columns = [
|
||||
{ type: 'db.nestedRows', data: 'sortOrder', title: '序号', width: 150 },
|
||||
{ type: 'text', data: 'name', title: '目录名称', required: true },
|
||||
]
|
||||
|
||||
const contextMenuSettings = {
|
||||
callback(key, selection, clickEvent){
|
||||
// Common callback for all options
|
||||
// console.log(key, selection, clickEvent);
|
||||
const row = selection[0].start.row
|
||||
if(key == 'add_child'){
|
||||
targetId.value = row
|
||||
}
|
||||
},
|
||||
items: {
|
||||
'row_above': {},
|
||||
'row_below': {},
|
||||
'__add_child': 'add_child',//nestedRows: true 单独设置'add_child'无法显示,只能在items中设置
|
||||
'space1': '---------',
|
||||
'remove_row': {
|
||||
async callback(key: string, selection: Selection[], clickEvent: MouseEvent){
|
||||
const row = selection[0].start.row;
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
// console.log(row,rowData)
|
||||
this.alter('remove_row', row, 1);
|
||||
if(rowData && rowData.id){
|
||||
await deleteDirectoryNode(rowData.id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
}
|
||||
},
|
||||
// 'space2': '---------',
|
||||
// 'option4': {
|
||||
// name() {
|
||||
// return '4444';
|
||||
// },
|
||||
// callback (key: string, selection: Selection[], clickEvent: MouseEvent) {
|
||||
// // this.alter('insert_row', coords.row + 1)
|
||||
// }
|
||||
// },
|
||||
}
|
||||
};
|
||||
|
||||
export let settings = {
|
||||
data: [],
|
||||
// dataSchema: initSchema(columns),
|
||||
colWidths: 200,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: contextMenuSettings,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
afterOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
if (event.detail === 1 && event.button === 0) {
|
||||
selectedRow.value = coords
|
||||
const rowData = this.getSourceDataAtRow(coords.row)
|
||||
if(rowData)hstRef?.value?.onCellMouseDownCallback(rowData)
|
||||
}
|
||||
},
|
||||
|
||||
afterChange(changes: any[], source: string) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return
|
||||
if(!validatorRow(this, changes, columns)) return;
|
||||
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
const targetRowData = this.getSourceDataAtRow(targetId.value)
|
||||
// 排除 sortOrder 字段,序号只显示不保存
|
||||
const data = {
|
||||
id: rowData.id ?? null,
|
||||
calcBaseRateCatalogId: hstRef?.value?.calcBaseRateCatalogId ?? null,
|
||||
parentId: targetRowData?.id ?? null,
|
||||
name: rowData.name,
|
||||
sortOrder: rowData.sortOrder,
|
||||
// attributes: {}
|
||||
}
|
||||
console.log('rowData', rowData, data)
|
||||
if (rowData.id == null) {
|
||||
createDirectoryNode(data).then(res => {
|
||||
hstRef?.value?.loadList()
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateDirectoryNode(data).then(res => {
|
||||
ElMessage.success('更新成功')
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'insert',
|
||||
name: '插入目录',
|
||||
callback: (hotInstance: any) => {
|
||||
if(!hstRef.value.calcBaseRateCatalogId) {
|
||||
ElMessage.error('请选择左侧树数据')
|
||||
return;
|
||||
}
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
}
|
||||
},
|
||||
]
|
||||
103
apps/web-ele/src/views/database/interface/cardinalRate/item.ts
Normal file
103
apps/web-ele/src/views/database/interface/cardinalRate/item.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { createRateItem, updateRateItem, deleteRateItem } from '#/api/database/interface/cardinalRate';
|
||||
import { validatorRow } from '#/components/db-hst/validator';
|
||||
const targetId = ref();
|
||||
let hstRef = ref();
|
||||
export function init(_hstRef: Ref<any>){
|
||||
hstRef = _hstRef
|
||||
}
|
||||
const columns = [
|
||||
{ type: 'text', data: 'sortOrder', title: '序号', width: 60 },
|
||||
{ type: 'text', data: 'name', title: '名称', required: true },
|
||||
{ type: 'text', data: 'rate', title: '费率' },
|
||||
// { type: 'text', data: 'calcBase', title: '计算基数' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
]
|
||||
|
||||
const contextMenuSettings = {
|
||||
// callback(key, selection, clickEvent) {
|
||||
// // Common callback for all options
|
||||
// console.log(key, selection, clickEvent);
|
||||
// },
|
||||
items: {
|
||||
'row_above': {},
|
||||
'row_below': {},
|
||||
'remove_row': {
|
||||
async callback(key: string, selection: Selection[], clickEvent: MouseEvent){
|
||||
const row = selection[0].start.row;
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
// console.log(row,rowData)
|
||||
this.alter('remove_row', row, 1);
|
||||
if(rowData && rowData.id){
|
||||
await deleteRateItem(rowData.id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
}
|
||||
},
|
||||
// 'space2': '---------',
|
||||
// 'option4': {
|
||||
// name() {
|
||||
// return '4444';
|
||||
// },
|
||||
// callback (key: string, selection: Selection[], clickEvent: MouseEvent) {
|
||||
// // this.alter('insert_row', coords.row + 1)
|
||||
// }
|
||||
// },
|
||||
}
|
||||
};
|
||||
|
||||
export let settings = {
|
||||
data: [],
|
||||
// dataSchema: initSchema(columns),
|
||||
colWidths: 100,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: contextMenuSettings,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
afterChange(changes: any[], source: string) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return
|
||||
if(!validatorRow(this, changes, columns)) return;
|
||||
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
// 排除 sortOrder 字段,序号只显示不保存
|
||||
const data = {
|
||||
...rowData,
|
||||
calcBaseRateDirectoryId: hstRef?.value?.calcBaseRateDirectoryId ?? null,
|
||||
// attributes: {}
|
||||
}
|
||||
console.log('rowData', rowData, data)
|
||||
if (rowData.id == null) {
|
||||
createRateItem(data).then(res => {
|
||||
hstRef?.value?.loadList()
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateRateItem(data).then(res => {
|
||||
ElMessage.success('更新成功')
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
export const contextMenuItems = [
|
||||
{
|
||||
key: 'insert',
|
||||
name: '插入目录',
|
||||
callback: (hotInstance: any) => {
|
||||
if(!hstRef.value.calcBaseRateDirectoryId) {
|
||||
ElMessage.error('请选择左侧树数据')
|
||||
return;
|
||||
}
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
}
|
||||
},
|
||||
]
|
||||
166
apps/web-ele/src/views/database/interface/cardinalRate/tree.ts
Normal file
166
apps/web-ele/src/views/database/interface/cardinalRate/tree.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// import {
|
||||
// createCatalogNode,
|
||||
// deleteCatalogNode,
|
||||
// type CardinalRateApi,
|
||||
// } from '#/api/database/interface/cardinalRate';
|
||||
import {
|
||||
createQuotaCatalogItem,
|
||||
deleteQuotaCatalogItem
|
||||
} from '#/api/database/quota/index';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
type MenuCallback = (command: string, node: any, data: any, treeInstance: any) => Promise<void>;
|
||||
|
||||
type MenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
divided?: boolean;
|
||||
callback: MenuCallback;
|
||||
};
|
||||
|
||||
type NodeConfig = {
|
||||
nodeType: string;
|
||||
name: string;
|
||||
codePrefix: string;
|
||||
};
|
||||
|
||||
// 生成唯一代码
|
||||
const generateCode = (prefix: string): string => `${prefix}-${Date.now()}`;
|
||||
|
||||
// 节点配置
|
||||
const ROOT_CONFIG: NodeConfig = { nodeType: 'root', name: '根节点', codePrefix: 'ROOT' };
|
||||
const PROVINCE_CONFIG: NodeConfig = { nodeType: 'province', name: '省市', codePrefix: 'PROVINCE' };
|
||||
const CONTENT_CONFIG: NodeConfig = { nodeType: 'content', name: '清单', codePrefix: 'CONTENT' };
|
||||
const MAJORS_CONFIG: NodeConfig = { nodeType: 'majors', name: '专业', codePrefix: 'MAJORS' };
|
||||
|
||||
// 创建菜单项
|
||||
const createMenuItem = (key: string, label: string, callback: MenuCallback, divided?: boolean): MenuItem => ({
|
||||
key,
|
||||
label,
|
||||
callback,
|
||||
...(divided && { divided }),
|
||||
});
|
||||
|
||||
// 添加根节点回调
|
||||
const createAddRootCallback = (config: NodeConfig): MenuCallback => {
|
||||
return async (_command, _node, _data, treeInstance) => {
|
||||
const source = {
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: treeInstance.getData().length + 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
treeInstance.addData({
|
||||
id: String(res.id),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('添加成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 添加子节点回调
|
||||
const createAddChildCallback = (config: NodeConfig): MenuCallback => {
|
||||
return async (_command, _node, data, treeInstance) => {
|
||||
const source = {
|
||||
parentId: data.id,
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: (data.children?.length || 0) + 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
treeInstance.addData({
|
||||
id: String(res),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('添加成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 插入节点回调
|
||||
const createInsertCallback = (config: NodeConfig, position: 'above' | 'below'): MenuCallback => {
|
||||
return async (_command, node, data, treeInstance) => {
|
||||
const source = {
|
||||
parentId: node.parent?.data?.id || null,
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: data.sortOrder || 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
const insertMethod = position === 'above' ? 'insertAbove' : 'insertBelow';
|
||||
treeInstance[insertMethod]({
|
||||
id: String(res),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('插入成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 删除节点回调
|
||||
const createDeleteCallback = (): MenuCallback => {
|
||||
return async (_command, _node, data, treeInstance) => {
|
||||
await deleteQuotaCatalogItem(data.id);
|
||||
treeInstance.removeData();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 根节点菜单 - 空树时显示
|
||||
export const rootMenus = [
|
||||
// {
|
||||
// key: 'add-root',
|
||||
// label: '添加根节点',
|
||||
// callback: createAddRootCallback(ROOT_CONFIG),
|
||||
// },
|
||||
];
|
||||
|
||||
// 节点层级菜单
|
||||
export const nodeMenus = [
|
||||
// {
|
||||
// level: 1, // 根节点层级
|
||||
// items: [
|
||||
// createMenuItem('add-province', '添加省市', createAddChildCallback(PROVINCE_CONFIG)),
|
||||
// createMenuItem('remove-root', '删除', createDeleteCallback(), true),
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// level: 2, // 省市节点层级
|
||||
// items: [
|
||||
// createMenuItem('add-province-above', '上方添加省市', createInsertCallback(PROVINCE_CONFIG, 'above')),
|
||||
// createMenuItem('add-province-below', '下方添加省市', createInsertCallback(PROVINCE_CONFIG, 'below')),
|
||||
// createMenuItem('add-content', '添加清单', createAddChildCallback(CONTENT_CONFIG)),
|
||||
// createMenuItem('remove-province', '删除', createDeleteCallback(), true),
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
level: 3, // 内容节点层级(叶子节点)
|
||||
items: [
|
||||
// createMenuItem('add-content-above', '上方添加清单', createInsertCallback(CONTENT_CONFIG, 'above')),
|
||||
// createMenuItem('add-content-below', '下方添加清单', createInsertCallback(CONTENT_CONFIG, 'below')),
|
||||
|
||||
createMenuItem('add-cardinal-rate', '添加专业类别', createAddChildCallback(MAJORS_CONFIG)),
|
||||
// createMenuItem('remove-content', '删除', createDeleteCallback(), true),
|
||||
],
|
||||
},
|
||||
{
|
||||
level: 4, // 内容节点层级(叶子节点)
|
||||
items: [
|
||||
createMenuItem('add-cardinal-rate-above', '上方添加专业类别', createInsertCallback(MAJORS_CONFIG, 'above')),
|
||||
createMenuItem('add-cardinal-rate-below', '下方添加专业类别', createInsertCallback(MAJORS_CONFIG, 'below')),
|
||||
|
||||
createMenuItem('remove-cardinal-rate', '删除', createDeleteCallback(), true),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,204 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch,computed, readonly } from 'vue'
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch, computed, readonly } from 'vue'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElSplitter,ElSplitterPanel,ElCard } from 'element-plus';
|
||||
import { getIndustryOptions } from '#/api/database/interface/project';
|
||||
import { ElSplitter, ElSplitterPanel, ElCard } from 'element-plus';
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
|
||||
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 bottomContainerRef = ref<HTMLElement | null>(null)
|
||||
const { height: bottomContainerHeight } = useElementSize(bottomContainerRef)
|
||||
import type { DetailedSettings } from 'handsontable/plugins/contextMenu';
|
||||
|
||||
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 tagRenderer = (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
// 清空单元格内容
|
||||
td.innerHTML = ''
|
||||
td.style.padding = '4px'
|
||||
td.style.overflow = 'visible'
|
||||
|
||||
// 获取当前单元格的标签数据
|
||||
const getCurrentTags = (): string[] => {
|
||||
const currentValue = instance.getDataAtCell(row, col)
|
||||
// console.log(currentValue)
|
||||
if (typeof currentValue === 'string') {
|
||||
return currentValue ? currentValue.split(',').map(t => t.trim()).filter(t => t) : []
|
||||
} else if (Array.isArray(currentValue)) {
|
||||
return [...currentValue]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 创建容器
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = 'display: flex; flex-wrap: wrap; gap: 4px; align-items: center; min-height: 24px;'
|
||||
|
||||
// 渲染标签
|
||||
const renderTags = () => {
|
||||
const tags = getCurrentTags()
|
||||
container.innerHTML = ''
|
||||
|
||||
tags.forEach((tag, index) => {
|
||||
const tagEl = document.createElement('span')
|
||||
tagEl.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
gap: 4px;
|
||||
`
|
||||
tagEl.textContent = tag
|
||||
|
||||
// 删除按钮
|
||||
const closeBtn = document.createElement('span')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
margin-left: 2px;
|
||||
`
|
||||
closeBtn.onmouseover = () => closeBtn.style.color = '#333'
|
||||
closeBtn.onmouseout = () => closeBtn.style.color = '#999'
|
||||
closeBtn.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
const currentTags = getCurrentTags()
|
||||
currentTags.splice(index, 1)
|
||||
instance.setDataAtCell(row, col, currentTags.join(','))
|
||||
renderTags()
|
||||
}
|
||||
|
||||
tagEl.appendChild(closeBtn)
|
||||
container.appendChild(tagEl)
|
||||
})
|
||||
|
||||
// 添加输入框
|
||||
const inputWrapper = document.createElement('span')
|
||||
inputWrapper.style.cssText = 'display: inline-flex; align-items: center;'
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.type = 'text'
|
||||
input.placeholder = '按Enter回车键添加'
|
||||
//border: 1px solid #d9d9d9;
|
||||
input.style.cssText = `
|
||||
const categoryTreeData = ref<any[]>([])
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
input.onfocus = () => {
|
||||
input.style.borderColor = '#409eff'
|
||||
}
|
||||
|
||||
input.onblur = () => {
|
||||
input.style.borderColor = '#d9d9d9'
|
||||
|
||||
// 失去焦点时添加标签
|
||||
if (input.value.trim()) {
|
||||
const newTag = input.value.trim()
|
||||
const currentTags = getCurrentTags()
|
||||
console.log('添加前的标签:', currentTags)
|
||||
if (!currentTags.includes(newTag)) {
|
||||
currentTags.push(newTag)
|
||||
console.log('添加后的标签:', currentTags)
|
||||
instance.setDataAtCell(row, col, currentTags.join(','))
|
||||
input.value = ''
|
||||
renderTags()
|
||||
} else {
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
// 保留 Enter 键功能,按 Enter 也可以添加
|
||||
if (e.key === 'Enter' && input.value.trim()) {
|
||||
e.preventDefault()
|
||||
input.blur() // 触发失去焦点事件
|
||||
}
|
||||
}
|
||||
|
||||
inputWrapper.appendChild(input)
|
||||
container.appendChild(inputWrapper)
|
||||
}
|
||||
|
||||
renderTags()
|
||||
td.appendChild(container)
|
||||
|
||||
return td
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
}
|
||||
|
||||
const bottomColumns = ref<any[]>([
|
||||
{type:'text',data:'code',title:'序号'},
|
||||
{type:'text',data:'name',title:'名称'},
|
||||
{type:'text',data:'content',title:'内容',width: 300, renderer: tagRenderer, readOnly:true},
|
||||
{type:'text',data:'spec',title:'代码'},
|
||||
const columns = [
|
||||
{ type: 'db.nestedRows', data: 'number', title: '序号', width: 100 },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'content', title: '内容', width: 700, readOnly: true },//renderer: tagRenderer
|
||||
{ type: 'text', data: 'code', title: '代号' },
|
||||
|
||||
])
|
||||
]
|
||||
// const colHeaders = ref<string[]>(topColHeaders)
|
||||
|
||||
const bottomHstRef = ref<any>(null)
|
||||
const hstRef = ref<any>(null)
|
||||
const contextMenuSettings: DetailedSettings = {
|
||||
callback(key, selection, clickEvent) {
|
||||
// Common callback for all options
|
||||
console.log(key, selection, clickEvent);
|
||||
|
||||
// dataManager.value.setData(dataManager.value.getData());
|
||||
// dataManager.value.updateWithData(dataInstance);
|
||||
// console.log(dataManager.value,dataManager.value.getData());
|
||||
// dataManager.value.updateWithData(dataManager.value.getData());
|
||||
},
|
||||
items: {
|
||||
'row_above': {},
|
||||
'row_below': {},
|
||||
'__add_child': 'add_child',//nestedRows: true 单独设置'add_child'无法显示,只能在items中设置
|
||||
'space1': '---------',
|
||||
'remove_row': {},
|
||||
// 'space2': '---------',
|
||||
// 'option4': {
|
||||
// name() {
|
||||
// return '4444';
|
||||
// },
|
||||
// callback (key: string, selection: Selection[], clickEvent: MouseEvent) {
|
||||
// // this.alter('insert_row', coords.row + 1)
|
||||
// }
|
||||
// },
|
||||
}
|
||||
};
|
||||
|
||||
const bootomMock = ()=>{
|
||||
// 生成模拟数据
|
||||
const mockData = Array.from({ length: 30 }, (_, index) => ({
|
||||
code: `DTL${String(index + 1).padStart(6, '0')}`,
|
||||
name: `明细项目${index + 1}`,
|
||||
content: ``,
|
||||
spec: `规格${index + 1}`,
|
||||
}))
|
||||
return mockData;
|
||||
}
|
||||
let bottomDbSettings = {
|
||||
columns: bottomColumns.value,
|
||||
data: [],
|
||||
// dataSchema: initSchema(columns),
|
||||
colWidths: 150,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: contextMenuSettings,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
// afterOnCellMouseDown(event: MouseEvent, coords: any, TD: HTMLTableCellElement){
|
||||
// if (event.detail === 1 && event.button === 0) {
|
||||
|
||||
// }
|
||||
// },
|
||||
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)
|
||||
// 排除 sortOrder 字段,序号只显示不保存
|
||||
const { sortOrder, ...dataWithoutSortOrder } = rowData
|
||||
let sendData: any = {
|
||||
}
|
||||
console.log('rowData', rowData, sendData)
|
||||
if (rowData.id == null) {
|
||||
// createItemTree(sendData).then(res => {
|
||||
// this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
// ElMessage.success('新增成功')
|
||||
// }).catch(err => {
|
||||
// console.error('新增失败', err)
|
||||
// })
|
||||
} else {
|
||||
// updateItemTree(sendData).then(res => {
|
||||
// console.log('updateResourceItems', res)
|
||||
// }).catch(err => {
|
||||
// console.error('更新失败', err)
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
const categoryHandleSelect = (node: Tree) => {
|
||||
console.log('categoryhandleSelect',node)
|
||||
const contextMenuItems = [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
// handleRowOperation(hotInstance, 'append')
|
||||
// // 等待 DOM 更新后重新渲染以应用验证样式
|
||||
// nextTick(() => {
|
||||
// hotInstance.render()
|
||||
// })
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
}
|
||||
},
|
||||
]
|
||||
const categoryHandleSelect = async (payload: { node: any, data: any, event: MouseEvent }) => {
|
||||
const { node, data, event } = payload
|
||||
console.log('categoryhandleSelect', node, data, event)
|
||||
|
||||
}
|
||||
|
||||
const detailHandleSelect = (node: Tree) => {
|
||||
@@ -207,23 +129,46 @@ const detailHandleSelect = (node: Tree) => {
|
||||
// // topHstRef.value.loadData(topHstData.value)
|
||||
// }
|
||||
}
|
||||
function onBottomHeight(height: number){
|
||||
if (bottomHstRef.value?.hotInstance) {
|
||||
bottomHstRef.value.hotInstance.updateSettings({
|
||||
height: height-15
|
||||
})
|
||||
|
||||
bottomHstRef.value.loadData(bootomMock())
|
||||
bottomHstRef.value.hotInstance.render()
|
||||
console.log('onResizeEnd-bottomHstRef',height);
|
||||
|
||||
const loadTreeData = async () => {
|
||||
try {
|
||||
const res = await getIndustryOptions()
|
||||
categoryTreeData.value = res || []
|
||||
} catch (error) {
|
||||
console.error('加载行业树数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
onBottomHeight(bottomContainerHeight.value)
|
||||
}, 200);
|
||||
loadTreeData()
|
||||
// load(bootomMock())
|
||||
// const mockData: any[] = [
|
||||
// {
|
||||
// __children: [
|
||||
// { code: '1-1', name: '项目编号', content: '', spec: '', type: 'text' },
|
||||
// { code: '1-2', name: '项目名称', content: '', spec: '', type: 'text' },
|
||||
// { code: '1-3', name: '行业', content: '建筑,市政,水利', spec: '', type: 'select' },
|
||||
// { code: '1-4', name: '信息价专业', content: '土建,安装,装饰', spec: '', type: 'select' },
|
||||
// { code: '1-5', name: '信息价地区', content: '广东,广西,湖南', spec: '', type: 'select' },
|
||||
// { code: '1-6', name: '信息价时间', content: '', spec: '', type: 'text' },
|
||||
// { code: '1-7', name: '造价类型', content: '概算,预算,结算', spec: '', type: 'select' },
|
||||
// { code: '1-8', name: '工作内容', content: '设计,施工,监理', spec: '', type: 'checkbox' },
|
||||
// { code: '1-9', name: '务与人员', content: '', spec: '', type: 'checkbox' },
|
||||
// ],
|
||||
// code: '1',
|
||||
// name: '工程信息',
|
||||
// content: '',
|
||||
// spec: '',
|
||||
// type: 'text',
|
||||
// }
|
||||
// ]
|
||||
|
||||
// setTimeout(() => {
|
||||
|
||||
// hstRef?.value.nestedRowsLoadData(mockData)
|
||||
// console.log('nestedRows', hstRef?.value.hotInstance?.getPlugin('nestedRows').enabled)
|
||||
// }, 400)
|
||||
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
@@ -231,22 +176,22 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter >
|
||||
<Page auto-content-height>
|
||||
<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 class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<!-- <DbTree :data="categoryTreeData" @select="categoryHandleSelect"
|
||||
:defaultExpandedKeys="2" :search="false" /> -->
|
||||
<DbTree ref="detailTreeRef" :tree-data="categoryTreeData" :tree-props="treeProps" @node-click="categoryHandleSelect" :defaultExpandedLevel="2"/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||
<DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst>
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="bottomDbSettings" :contextMenuItems="contextMenuItems"></DbHst>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
</style>
|
||||
<style lang="css"></style>
|
||||
|
||||
@@ -1,77 +1,65 @@
|
||||
<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,ElCard,ElTabs,ElTabPane } from 'element-plus';
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { getQuotaCatalogItemTree } from '#/api/database/quota/index';
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
// import { sourceDataObject } from '#/components/db-hst/mockData'
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElCard, ElSplitter, ElSplitterPanel } from 'element-plus';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
// 导入子组件
|
||||
import FieldName from './unit/FieldName.vue'
|
||||
import SubItem from './unit/SubItem.vue'
|
||||
import MeasureItem from './unit/MeasureItem.vue'
|
||||
import OtherItem from './unit/OtherItem.vue'
|
||||
import UnitSummary from './unit/UnitSummary.vue'
|
||||
import VariableSettings from './unit/VariableSettings.vue'
|
||||
import MaterialField from './unit/MaterialField.vue'
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const { height: containerHeight } = useElementSize(containerRef)
|
||||
// const topContainerRef = ref<HTMLElement | null>(null)
|
||||
// const { height: topContainerHeight } = useElementSize(topContainerRef)
|
||||
// const bottomContainerRef = ref<HTMLElement | null>(null)
|
||||
// const { height: bottomContainerHeight } = useElementSize(bottomContainerRef)
|
||||
// const bottomPanelHeight = ref<number>(0)
|
||||
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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
import VariableSettings from './unit/VariableSettings.vue';
|
||||
|
||||
type Tree = { id: string; label: string; children?: Tree[];[key: string]: any }
|
||||
const treeData = ref<Tree[]>([])
|
||||
const selectedNode = ref<any>(null)
|
||||
|
||||
// 转换后端数据为树形结构(后端已通过 exclude: 'rate_mode' 过滤掉费率模式节点)
|
||||
const transformTreeData = (data: any[]): Tree[] => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn('transformTreeData: 数据不是数组', data)
|
||||
return []
|
||||
}
|
||||
])
|
||||
// const colHeaders = ref<string[]>(topColHeaders)
|
||||
const activeTab = ref('fieldName')
|
||||
const categoryHandleSelect = (node: Tree) => {
|
||||
console.log('categoryhandleSelect',node)
|
||||
|
||||
return data.map(item => {
|
||||
const treeNode: Tree = {
|
||||
...item,
|
||||
id: String(item.id),
|
||||
label: item.name || item.label || '未命名',
|
||||
}
|
||||
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
treeNode.children = transformTreeData(item.children)
|
||||
}
|
||||
|
||||
return treeNode
|
||||
})
|
||||
}
|
||||
|
||||
const detailHandleSelect = (node: Tree) => {
|
||||
// if (topHstRef.value && typeof topHstRef.value.loadData === 'function') {
|
||||
// // console.log('hstData.value',hstData.value)
|
||||
// // topHstRef.value.loadData(topHstData.value)
|
||||
// }
|
||||
// 加载定额专业树(排除 rate_mode 节点,与定额基价页面一致)
|
||||
const loadCategoryTree = async () => {
|
||||
try {
|
||||
const res = await getQuotaCatalogItemTree({ exclude: 'rate_mode,majors,fields_majors' })
|
||||
console.log('定额专业树原始数据:', res)
|
||||
treeData.value = transformTreeData(res)
|
||||
console.log('定额专业树转换后数据:', treeData.value)
|
||||
} catch (error) {
|
||||
console.error('加载定额专业树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理树节点点击(只有 specialty 节点才显示变量设置)
|
||||
const handleNodeClick = (payload: { node: any, data: any, event: MouseEvent }) => {
|
||||
const { node, data } = payload
|
||||
console.log('节点点击:', node, data)
|
||||
// 只有定额专业节点才显示变量设置
|
||||
if (data.nodeType === 'specialty') {
|
||||
selectedNode.value = data
|
||||
} else {
|
||||
selectedNode.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategoryTree()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
@@ -80,50 +68,29 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- <DbHst ref="bottomHstRef" :settings="bottomDbSettings"></DbHst> -->
|
||||
<ElSplitter >
|
||||
<ElSplitterPanel collapsible 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" />
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<DbTree
|
||||
ref="treeRef"
|
||||
:tree-data="treeData"
|
||||
:draggable="false"
|
||||
:default-expanded-level="3"
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel collapsible :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full">
|
||||
<ElTabs v-model="activeTab" type="border-card" class="h-full">
|
||||
<ElTabPane label="字段名称" name="fieldName" lazy>
|
||||
<FieldName :height="containerHeight"/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="分部分项" name="subItem" lazy>
|
||||
<!-- <SubItem /> -->
|
||||
<FieldName :height="containerHeight"/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="措施项目" name="measureItem" lazy>
|
||||
<!-- <MeasureItem /> -->
|
||||
<FieldName :height="containerHeight"/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="其他项目" name="otherItem" lazy>
|
||||
<!-- <OtherItem /> -->
|
||||
<FieldName :height="containerHeight"/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="单位汇总" name="unitSummary" lazy>
|
||||
<!-- <UnitSummary /> -->
|
||||
<FieldName :height="containerHeight"/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="变量设置" name="variableSettings" lazy>
|
||||
<VariableSettings />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="工料机字段" name="materialField" lazy>
|
||||
<MaterialField />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full">
|
||||
<VariableSettings :selectedNode="selectedNode"/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
<style lang="scss">
|
||||
.el-tabs--border-card>.el-tabs__content {
|
||||
padding: 5px 0;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog, ElInput, ElTabPane, ElTabs } from 'element-plus';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
createDivisionSettings,
|
||||
createMeasureSettings,
|
||||
createOtherSettings,
|
||||
createUnitSummarySettings,
|
||||
} from './cardinal';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
rowData?: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success', value: any): void
|
||||
}>()
|
||||
|
||||
const isVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const activeTab = ref('division')
|
||||
const formulaInput = ref('')
|
||||
|
||||
const settingsByTab = {
|
||||
division: createDivisionSettings({ formulaInput }),
|
||||
measure: createMeasureSettings({ formulaInput }),
|
||||
other: createOtherSettings({ formulaInput }),
|
||||
supplement: createUnitSummarySettings({ formulaInput }),
|
||||
} as const
|
||||
const handleSubmit = () => {
|
||||
emit('success', formulaInput.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
formulaInput.value = ''
|
||||
return
|
||||
}
|
||||
activeTab.value = 'division'
|
||||
const currentCardinal = props.rowData?.cardinal
|
||||
formulaInput.value = currentCardinal == null ? '' : String(currentCardinal)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog v-model="isVisible" title="费用代码设置" width="60%" :close-on-click-modal="false" body-class="fee-body-height">
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<!-- <div style="color: #909399; font-size: 13px; margin-bottom: 8px;">
|
||||
提示: 现在书写的公式只能调用在当前项目以前的取费和系统参数
|
||||
</div> -->
|
||||
<ElInput v-model="formulaInput" placeholder="请输入公式" clearable />
|
||||
|
||||
|
||||
<ElTabs v-model="activeTab" size="small" class="!p-0 h-full w-full fee-tabs">
|
||||
<!-- <ElTabPane label="全局费用" name="global">
|
||||
<ElTable :data="tabTableData.global" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane> -->
|
||||
|
||||
<ElTabPane label="分部分项" name="division" lazy class="h-full">
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst :settings="settingsByTab.division" v-if="isVisible" />
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="措施费" name="measure" lazy class="h-full">
|
||||
<DbHst :settings="settingsByTab.measure" v-if="isVisible" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="其他项目" name="other" lazy class="h-full">
|
||||
<DbHst :settings="settingsByTab.other" v-if="isVisible" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="单位汇总" name="supplement" lazy class="h-full">
|
||||
<DbHst :settings="settingsByTab.supplement" v-if="isVisible" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="isVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
<style lang="scss" >
|
||||
.fee-body-height{
|
||||
height: 500px;
|
||||
}
|
||||
.fee-tabs{
|
||||
.el-tabs__header{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,150 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
}>()
|
||||
const hstRef = ref<any>(null)
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'code', title: '编码', renderer: codeRenderer, code:true },
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'feature', title: '项目特征' },
|
||||
{ type: 'text', data: 'locked', title: '锁定' },
|
||||
{ type: 'text', data: 'unitPrice', title: '综合单价' },
|
||||
{ type: 'text', data: 'unit', title: '单位' },
|
||||
{ type: 'text', data: 'quantity', title: '工程量' },
|
||||
{ type: 'text', data: 'comprehensivePrice', title: '综合单价' },
|
||||
{ type: 'text', data: 'totalPrice', title: '综合合价' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
])
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data ) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
|
||||
const mockData = (() => {
|
||||
const data: any[] = []
|
||||
|
||||
// 生成5个父级数据
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const parent = {
|
||||
seq: `${i}`,
|
||||
code: `CODE${String(i).padStart(6, '0')}`,
|
||||
category: i % 3 === 0 ? '分部分项' : i % 3 === 1 ? '措施项目' : '其他项目',
|
||||
name: `项目名称${i}`,
|
||||
feature: `特征描述${i}`,
|
||||
locked: i % 2 === 0 ? '是' : '否',
|
||||
unitPrice: `${i * 100}`,
|
||||
unit: 'm²',
|
||||
quantity: `${i * 10}`,
|
||||
comprehensivePrice: `${i * 100}`,
|
||||
totalPrice: `${i * 1000}`,
|
||||
remark: `备注${i}`,
|
||||
level: String(i - 1),
|
||||
__children: [] as any[]
|
||||
}
|
||||
|
||||
// 为每个父级生成3-5个子级
|
||||
const childCount = Math.floor(Math.random() * 3) + 3
|
||||
for (let j = 1; j <= childCount; j++) {
|
||||
const child = {
|
||||
seq: `${i}.${j}`,
|
||||
code: `CODE${String(i).padStart(6, '0')}-${String(j).padStart(3, '0')}`,
|
||||
category: '子项',
|
||||
name: `子项目${i}-${j}`,
|
||||
feature: `子特征${j}`,
|
||||
locked: '否',
|
||||
unitPrice: `${j * 50}`,
|
||||
unit: 'm²',
|
||||
quantity: `${j * 5}`,
|
||||
comprehensivePrice: `${j * 50}`,
|
||||
totalPrice: `${j * 250}`,
|
||||
remark: `子备注${j}`,
|
||||
level: `${i - 1}.${j - 1}`,
|
||||
__children: []
|
||||
}
|
||||
parent.__children.push(child)
|
||||
}
|
||||
|
||||
data.push(parent)
|
||||
}
|
||||
|
||||
return data
|
||||
})()
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns: columns.value,
|
||||
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: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
watch(
|
||||
() => props.height,
|
||||
(newHeight) => {
|
||||
console.log('MarketMaterials newHeight', newHeight)
|
||||
if (newHeight && hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
})
|
||||
hstRef.value.hotInstance.render()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// if (hstRef.value) {
|
||||
// hstRef.value.loadData(mockData)
|
||||
// }
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text' | 'checkbox'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60 },
|
||||
{ type: 'text', data: 'category', title: '类别', width: 120 },
|
||||
{ type: 'text', data: 'name', title: '名称', width: 260 },
|
||||
{ type: 'text', data: 'feature', title: '项目特征', width: 220 },
|
||||
{ type: 'checkbox', data: 'isLockedUnitPrice', title: '锁定综合单价', width: 120 },
|
||||
{ type: 'text', data: 'unit', title: '单位', width: 80 },
|
||||
{ type: 'text', data: 'quantity', title: '工程量', width: 100 },
|
||||
{ type: 'text', data: 'unitPrice', title: '综合单价', width: 110 },
|
||||
{ type: 'text', data: 'totalPrice', title: '综合合价', width: 120 },
|
||||
]
|
||||
|
||||
const createRowSchema = (cols: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
cols.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const rowSchema = createRowSchema(columns)
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
category: `类别${((rowNumber - 1) % 5) + 1}`,
|
||||
name: `分部分项${rowNumber}`,
|
||||
feature: `特征${((rowNumber - 1) % 3) + 1}`,
|
||||
isLockedUnitPrice: false,
|
||||
unit: 'm²',
|
||||
quantity: String((rowNumber * 10).toFixed(2)),
|
||||
unitPrice: String((rowNumber * 12.5).toFixed(2)),
|
||||
totalPrice: String((rowNumber * 10 * 12.5).toFixed(2)),
|
||||
}
|
||||
})
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function (this: any, key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
} else {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '序号' },
|
||||
{ type: 'text', data: 'name', title: '字段名称' },
|
||||
{ type: 'checkbox', data: 'unit', title: '分部分项隐藏' },
|
||||
{ type: 'checkbox', data: 'price', title: '措施项目隐藏' },
|
||||
])
|
||||
|
||||
const settings = {
|
||||
columns: columns.value,
|
||||
}
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `MAT${String(index + 1).padStart(6, '0')}`,
|
||||
name: `工料机${index + 1}`,
|
||||
unit: false,
|
||||
price: false,
|
||||
}))
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (hstRef.value) {
|
||||
hstRef.value.loadData(mockData)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
95
apps/web-ele/src/views/database/interface/unit/Materials.vue
Normal file
95
apps/web-ele/src/views/database/interface/unit/Materials.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'code', title: '字段名称',},
|
||||
{ type: 'checkbox', data: 'category', title: '分部分项隐藏' },
|
||||
{ type: 'checkbox', data: 'name', title: '措施项目隐藏' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
])
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data ) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
|
||||
const mockData = (() => {
|
||||
const data: any[] = []
|
||||
|
||||
// 生成5个父级数据
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const parent = {
|
||||
seq: `${i}`,
|
||||
code: `CODE${String(i).padStart(6, '0')}`,
|
||||
category: false,
|
||||
name: false,
|
||||
feature: false,
|
||||
locked: false,
|
||||
remark: `备注${i}`,
|
||||
level: String(i - 1),
|
||||
__children: [] as any[]
|
||||
}
|
||||
data.push(parent)
|
||||
}
|
||||
|
||||
return data
|
||||
})()
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns: columns.value,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above:{
|
||||
},
|
||||
row_below: {
|
||||
},
|
||||
separator2: '---------',
|
||||
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)
|
||||
}
|
||||
},
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// if (hstRef.value) {
|
||||
// hstRef.value.loadData(mockData)
|
||||
// }
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { getCategoriesList } from '#/api/database/materials/root';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog, ElInput } from 'element-plus';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
rowData?: any
|
||||
catalogItemId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success', value: string): void
|
||||
}>()
|
||||
|
||||
const isVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const formulaInput = ref('')
|
||||
const hstRef = ref<any>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('success', formulaInput.value)
|
||||
isVisible.value = false
|
||||
}
|
||||
|
||||
// 加载工料机总类数据
|
||||
const loadCategoriesData = async () => {
|
||||
try {
|
||||
const res = await getCategoriesList()
|
||||
// 转换数据格式,映射到表格列
|
||||
tableData.value = (res || []).map((item: any, index: number) => ({
|
||||
code: String(index + 1),
|
||||
category: item.code || '',
|
||||
taxFreeBaseCode: item.taxExclBaseCode || '',
|
||||
taxIncludedBaseCode: item.taxInclBaseCode || '',
|
||||
taxFreeCompileCode: item.taxExclCompileCode || '',
|
||||
taxIncludedCompileCode: item.taxInclCompileCode || '',
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载工料机总类失败:', error)
|
||||
tableData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ type: 'text', data: 'code', title: '序号', width: 60, readOnly: true },
|
||||
{ type: 'text', data: 'category', title: '类别', readOnly: true },
|
||||
{ type: 'text', data: 'taxFreeBaseCode', title: '除税基价代码', readOnly: true },
|
||||
{ type: 'text', data: 'taxIncludedBaseCode', title: '含税基价代码', readOnly: true },
|
||||
{ type: 'text', data: 'taxFreeCompileCode', title: '除税编制代码', readOnly: true },
|
||||
{ type: 'text', data: 'taxIncludedCompileCode', title: '含税编制代码', readOnly: true },
|
||||
]
|
||||
|
||||
const allowedSelectionDataKeys = new Set([
|
||||
'taxFreeBaseCode',
|
||||
'taxIncludedBaseCode',
|
||||
'taxFreeCompileCode',
|
||||
'taxIncludedCompileCode',
|
||||
])
|
||||
|
||||
let settings = {
|
||||
data: [],
|
||||
colWidths: 150,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
selectionMode: 'single',
|
||||
className: 'htCenter',
|
||||
afterSelection(row: number, col: number, row2: number, col2: number) {
|
||||
if (row < 0 || col < 0) return
|
||||
const selectedDataKey = columns?.[col]?.data
|
||||
if (!selectedDataKey || !allowedSelectionDataKeys.has(selectedDataKey)) return
|
||||
const cellValue = this.getDataAtCell(row, col)
|
||||
if (cellValue == null) return
|
||||
formulaInput.value += String(cellValue)
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (visible) => {
|
||||
if (!visible) {
|
||||
formulaInput.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 加载工料机总类数据
|
||||
await loadCategoriesData()
|
||||
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if(hstRef?.value?.hotInstance) {
|
||||
hstRef?.value?.hotInstance.loadData(tableData.value)
|
||||
}
|
||||
}, 200);
|
||||
})
|
||||
|
||||
const currentCardinal = props.rowData?.cardinal
|
||||
formulaInput.value = currentCardinal == null ? '' : String(currentCardinal)
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog v-model="isVisible" title="工料机总类" width="60%" :close-on-click-modal="false" body-class="materials-body-height">
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<div>
|
||||
<ElInput v-model="formulaInput" placeholder="请输入计算基数公式" clearable></ElInput>
|
||||
</div>
|
||||
|
||||
<DbHst ref="hstRef" :settings="settings" v-if="isVisible"></DbHst>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="isVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.materials-body-height{
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import CardinalDialog from './CardinalDialog.vue'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '措施项目名称' },
|
||||
{ type: 'text', data: 'unit', title: '单位' },
|
||||
{ type: 'text', data: 'amount', title: '金额' },
|
||||
])
|
||||
const isBaseDialogVisible = ref(false)
|
||||
const currentRowData = ref<any>(null)
|
||||
const currentRowIndex = ref<number | null>(null)
|
||||
|
||||
const settings = {
|
||||
columns: columns.value,
|
||||
const openBaseDialog = (rowData: any, row: number) => {
|
||||
currentRowData.value = rowData == null ? null : { ...rowData, cardinal: rowData.base }
|
||||
currentRowIndex.value = row
|
||||
isBaseDialogVisible.value = true
|
||||
}
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `MSR${String(index + 1).padStart(6, '0')}`,
|
||||
name: `措施项目${index + 1}`,
|
||||
unit: '项',
|
||||
amount: `${(index + 1) * 1000}`,
|
||||
}))
|
||||
const handleBaseDialogSuccess = (formula: string) => {
|
||||
const hot = hstRef.value?.hotInstance
|
||||
if (!hot) return
|
||||
if (currentRowIndex.value == null) return
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (hstRef.value) {
|
||||
hstRef.value.loadData(mockData)
|
||||
}
|
||||
}, 100)
|
||||
hot.setDataAtRowProp(currentRowIndex.value, 'base', formula, 'baseDialog')
|
||||
hot.render()
|
||||
isBaseDialogVisible.value = false
|
||||
}
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text' | 'checkbox'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
renderer?: any
|
||||
}
|
||||
|
||||
const baseRenderer = (
|
||||
instance: any,
|
||||
td: HTMLTableCellElement,
|
||||
row: number,
|
||||
_col: number,
|
||||
_prop: string | number,
|
||||
value: any,
|
||||
_cellProperties: any,
|
||||
) => {
|
||||
td.innerHTML = ''
|
||||
td.style.padding = '0 8px'
|
||||
td.style.display = 'flex'
|
||||
td.style.alignItems = 'center'
|
||||
td.style.justifyContent = 'space-between'
|
||||
td.style.gap = '8px'
|
||||
td.style.boxSizing = 'border-box'
|
||||
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value == null ? '' : String(value)
|
||||
textSpan.style.flex = '1 1 auto'
|
||||
textSpan.style.minWidth = '0'
|
||||
textSpan.style.overflow = 'hidden'
|
||||
textSpan.style.textOverflow = 'ellipsis'
|
||||
textSpan.style.whiteSpace = 'nowrap'
|
||||
|
||||
const iconButton = document.createElement('button')
|
||||
iconButton.type = 'button'
|
||||
iconButton.title = '编辑基数'
|
||||
iconButton.setAttribute('aria-label', '编辑基数')
|
||||
iconButton.style.display = 'inline-flex'
|
||||
iconButton.style.alignItems = 'center'
|
||||
iconButton.style.justifyContent = 'center'
|
||||
iconButton.style.width = '18px'
|
||||
iconButton.style.height = '18px'
|
||||
iconButton.style.flex = '0 0 auto'
|
||||
iconButton.style.border = '0'
|
||||
iconButton.style.background = 'transparent'
|
||||
iconButton.style.padding = '0'
|
||||
iconButton.style.cursor = 'pointer'
|
||||
iconButton.style.color = '#909399'
|
||||
iconButton.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="5" cy="12" r="2" fill="currentColor" /><circle cx="12" cy="12" r="2" fill="currentColor" /><circle cx="19" cy="12" r="2" fill="currentColor" /></svg>'
|
||||
|
||||
iconButton.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
iconButton.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const rowData = instance.getSourceDataAtRow?.(row)
|
||||
openBaseDialog(rowData, row)
|
||||
})
|
||||
|
||||
td.appendChild(textSpan)
|
||||
td.appendChild(iconButton)
|
||||
return td
|
||||
}
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60 },
|
||||
{ type: 'text', data: 'code', title: '编码', width: 130 },
|
||||
{ type: 'text', data: 'category', title: '类别', width: 120 },
|
||||
{ type: 'checkbox', data: 'adjustment', title: '调整', width: 80 },
|
||||
{ type: 'checkbox', data: 'reduction', title: '降效', width: 80 },
|
||||
{ type: 'text', data: 'name', title: '名称', width: 260 },
|
||||
{ type: 'text', data: 'feature', title: '项目特征', width: 220 },
|
||||
{ type: 'text', data: 'specification', title: '规格', width: 140 },
|
||||
{ type: 'text', data: 'unit', title: '单位', width: 80 },
|
||||
{ type: 'text', data: 'quantity', title: '工程量', width: 100 },
|
||||
{ type: 'text', data: 'liao', title: '聊', width: 90 },
|
||||
{ type: 'checkbox', data: 'isExcludedIndicator', title: '不计指标', width: 90 },
|
||||
{ type: 'text', data: 'base', title: '基数', width: 110, renderer: baseRenderer },
|
||||
{ type: 'text', data: 'rate', title: '费率', width: 90 },
|
||||
]
|
||||
|
||||
const createRowSchema = (cols: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
cols.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const rowSchema = createRowSchema(columns)
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const quantity = rowNumber * 5
|
||||
const base = rowNumber * 100
|
||||
const rate = (rowNumber % 5) * 0.02
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
code: `CS${String(rowNumber).padStart(6, '0')}`,
|
||||
category: `类别${((rowNumber - 1) % 4) + 1}`,
|
||||
adjustment: rowNumber % 3 === 0 ? true : false,
|
||||
reduction: rowNumber % 4 === 0 ? true : false,
|
||||
name: `措施项目${rowNumber}`,
|
||||
feature: `特征${((rowNumber - 1) % 3) + 1}`,
|
||||
specification: `规格${((rowNumber - 1) % 6) + 1}`,
|
||||
unit: '项',
|
||||
quantity: String(quantity.toFixed(2)),
|
||||
liao: String((rowNumber * 1.2).toFixed(2)),
|
||||
isExcludedIndicator: rowNumber % 7 === 0,
|
||||
base: String(base.toFixed(2)),
|
||||
rate: String((rate * 100).toFixed(2)) + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function (this: any, key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
} else {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
<CardinalDialog v-model="isBaseDialogVisible" :rowData="currentRowData" @success="handleBaseDialogSuccess" />
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '其他项目名称' },
|
||||
{ type: 'text', data: 'type', title: '类型' },
|
||||
{ type: 'text', data: 'amount', title: '金额' },
|
||||
])
|
||||
|
||||
const settings = {
|
||||
columns: columns.value,
|
||||
type ColumnConfig = {
|
||||
type: 'text' | 'checkbox'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `OTH${String(index + 1).padStart(6, '0')}`,
|
||||
name: `其他项目${index + 1}`,
|
||||
type: '其他',
|
||||
amount: `${(index + 1) * 500}`,
|
||||
}))
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60 },
|
||||
{ type: 'text', data: 'name', title: '名称', width: 260 },
|
||||
{ type: 'text', data: 'base', title: '计算基数', width: 120 },
|
||||
{ type: 'text', data: 'rate', title: '费率', width: 90 },
|
||||
{ type: 'text', data: 'amount', title: '金额', width: 120 },
|
||||
{ type: 'text', data: 'feeCategory', title: '费用类别', width: 120 },
|
||||
{ type: 'checkbox', data: 'isNonCompetitiveFee', title: '不可竞争费', width: 100 },
|
||||
{ type: 'checkbox', data: 'isExcludedFromTotal', title: '不计入合价', width: 100 },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (hstRef.value) {
|
||||
hstRef.value.loadData(mockData)
|
||||
}
|
||||
}, 100)
|
||||
const createRowSchema = (cols: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
cols.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const rowSchema = createRowSchema(columns)
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const base = rowNumber * 100
|
||||
const rate = (rowNumber % 6) * 0.015
|
||||
const amount = base * rate
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
name: `其他项目${rowNumber}`,
|
||||
base: String(base.toFixed(2)),
|
||||
rate: String((rate * 100).toFixed(2)) + '%',
|
||||
amount: String(amount.toFixed(2)),
|
||||
feeCategory: `类别${((rowNumber - 1) % 4) + 1}`,
|
||||
isNonCompetitiveFee: rowNumber % 5 === 0,
|
||||
isExcludedFromTotal: rowNumber % 7 === 0,
|
||||
}
|
||||
})
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function (this: any, key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
} else {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="h-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '项目名称' },
|
||||
{ type: 'text', data: 'unit', title: '单位' },
|
||||
{ type: 'text', data: 'quantity', title: '工程量' },
|
||||
])
|
||||
|
||||
const settings = {
|
||||
columns: columns.value,
|
||||
}
|
||||
|
||||
const mockData = Array.from({ length: 20 }, (_, index) => ({
|
||||
code: `SUB${String(index + 1).padStart(6, '0')}`,
|
||||
name: `分部分项${index + 1}`,
|
||||
unit: 'm²',
|
||||
quantity: `${(index + 1) * 10}`,
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (hstRef.value) {
|
||||
hstRef.value.loadData(mockData)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,38 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'text', data: 'count', title: '数量' },
|
||||
{ type: 'text', data: 'totalAmount', title: '合计金额' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
])
|
||||
|
||||
const settings = {
|
||||
columns: columns.value,
|
||||
type ColumnConfig = {
|
||||
type: 'text'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
const mockData = [
|
||||
{ category: '分部分项', count: '50', totalAmount: '500000', remark: '' },
|
||||
{ category: '措施项目', count: '20', totalAmount: '100000', remark: '' },
|
||||
{ category: '其他项目', count: '10', totalAmount: '50000', remark: '' },
|
||||
{ category: '合计', count: '80', totalAmount: '650000', remark: '' },
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60 },
|
||||
{ type: 'text', data: 'name', title: '名称', width: 260 },
|
||||
{ type: 'text', data: 'base', title: '计算基数', width: 120 },
|
||||
{ type: 'text', data: 'rate', title: '费率', width: 90 },
|
||||
{ type: 'text', data: 'total', title: '合计', width: 120 },
|
||||
{ type: 'text', data: 'code', title: '代号', width: 100 },
|
||||
{ type: 'text', data: 'remark', title: '备注', width: 200 },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (hstRef.value) {
|
||||
hstRef.value.loadData(mockData)
|
||||
}
|
||||
}, 100)
|
||||
const createRowSchema = (cols: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
cols.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const rowSchema = createRowSchema(columns)
|
||||
|
||||
const mockData = Array.from({ length: 10 }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const base = rowNumber * 1000
|
||||
const rate = (rowNumber % 5) * 0.01
|
||||
const total = base * rate
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
name: `单位汇总${rowNumber}`,
|
||||
base: String(base.toFixed(2)),
|
||||
rate: String((rate * 100).toFixed(2)) + '%',
|
||||
total: String(total.toFixed(2)),
|
||||
code: `DH${String(rowNumber).padStart(3, '0')}`,
|
||||
remark: rowNumber % 3 === 0 ? '备注' : '',
|
||||
}
|
||||
})
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
separator2: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function (this: any, key: string, selection: any[]) {
|
||||
const selectedRow = selection[0].start.row
|
||||
const rowData = this.getSourceDataAtRow(selectedRow)
|
||||
|
||||
if (rowData?.id) {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
} else {
|
||||
this.alter('remove_row', selectedRow, 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="h-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,267 +1,321 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ElDialog, ElTable, ElTableColumn, ElButton, ElTabs, ElTabPane, ElInput, ElSegmented } from 'element-plus'
|
||||
import {
|
||||
createVariableSetting,
|
||||
deleteVariableSetting,
|
||||
getVariableSettingList,
|
||||
updateVariableSetting,
|
||||
type VariableSettingVO
|
||||
} from '#/api/database/quota/variableSetting'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
import { handleRowOperation, codeRenderer } from '#/components/db-hst/tree'
|
||||
import { ElMessage, ElSegmented } from 'element-plus'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import MaterialsDialog from './MaterialsDialog.vue'
|
||||
import {
|
||||
createDbHstSettings,
|
||||
segmentedOptions,
|
||||
type ColumnConfig,
|
||||
type SegmentedType
|
||||
} from './segmentedSettings'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
// catalogItemId?: string;
|
||||
selectedNode?: any
|
||||
}>()
|
||||
const catalogItemId = computed(()=>props.selectedNode?.id ?? '')
|
||||
|
||||
// 类别映射
|
||||
const categoryMap: Record<string, string> = {
|
||||
'分部分项': 'division',
|
||||
'措施项目': 'measure',
|
||||
'其他项目': 'other',
|
||||
'单位汇总': 'unit_summary',
|
||||
}
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
const dialogVisible = ref(false)
|
||||
const activeTab = ref('global')
|
||||
const isDialogVisible = ref(false)
|
||||
const currentRowData = ref<any>(null)
|
||||
const formulaInput = ref('')
|
||||
const currentRowIndex = ref<number | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 各个 Tab 的表格数据
|
||||
const tabTableData = ref({
|
||||
global: [
|
||||
{ seq: '1', code: 'GF001', name: '全局费用1', value: '100' },
|
||||
{ seq: '2', code: 'GF002', name: '全局费用2', value: '200' },
|
||||
],
|
||||
division: [
|
||||
{ seq: '1', code: 'FB001', name: '分部分项1', value: '150' },
|
||||
{ seq: '2', code: 'FB002', name: '分部分项2', value: '250' },
|
||||
],
|
||||
measure: [
|
||||
{ seq: '1', code: 'CS001', name: '措施项目1', value: '180' },
|
||||
{ seq: '2', code: 'CS002', name: '措施项目2', value: '280' },
|
||||
],
|
||||
other: [
|
||||
{ seq: '1', code: 'QT001', name: '其他项目1', value: '120' },
|
||||
{ seq: '2', code: 'QT002', name: '其他项目2', value: '220' },
|
||||
],
|
||||
supplement: [
|
||||
{ seq: '1', code: 'BC001', name: '补充费用1', value: '160' },
|
||||
{ seq: '2', code: 'BC002', name: '补充费用2', value: '260' },
|
||||
],
|
||||
})
|
||||
|
||||
const dialogRenderer = (instance: any, td: HTMLElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
td.innerHTML = ''
|
||||
td.style.position = 'relative'
|
||||
td.style.padding = '0 8px'
|
||||
|
||||
// 创建文本容器
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.textContent = value || ''
|
||||
textSpan.style.display = 'inline-block'
|
||||
textSpan.style.verticalAlign = 'middle'
|
||||
textSpan.style.maxWidth = 'calc(100% - 24px)'
|
||||
textSpan.style.overflow = 'hidden'
|
||||
textSpan.style.textOverflow = 'ellipsis'
|
||||
textSpan.style.whiteSpace = 'nowrap'
|
||||
|
||||
// 创建图标按钮
|
||||
const iconBtn = document.createElement('span')
|
||||
iconBtn.innerHTML = '⚙️'
|
||||
iconBtn.style.cursor = 'pointer'
|
||||
iconBtn.style.marginLeft = '4px'
|
||||
iconBtn.style.fontSize = '14px'
|
||||
iconBtn.style.verticalAlign = 'middle'
|
||||
iconBtn.style.float = 'right'
|
||||
|
||||
iconBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const rowData = instance.getSourceDataAtRow(instance.toPhysicalRow(row))
|
||||
currentRowData.value = rowData
|
||||
dialogVisible.value = true
|
||||
})
|
||||
|
||||
td.appendChild(textSpan)
|
||||
td.appendChild(iconBtn)
|
||||
|
||||
return td
|
||||
const openDialog = (rowData: any, row: number) => {
|
||||
currentRowData.value = rowData
|
||||
currentRowIndex.value = row
|
||||
isDialogVisible.value = true
|
||||
}
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', renderer: codeRenderer , code:true},
|
||||
{ type: 'text', data: 'spec', title: '费用代码', width: 300,renderer: dialogRenderer },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数(用户端不显示)', width: 200 },
|
||||
])
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data ) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
|
||||
const mockData = (() => {
|
||||
const data: any[] = []
|
||||
|
||||
// 生成5个父级数据
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const parent: any = {
|
||||
seq: `${i}`,
|
||||
name: `费用名称${i}`,
|
||||
spec: `SPEC${String(i).padStart(3, '0')}`,
|
||||
cardinal: `基数${i}`,
|
||||
__children: [] as any[]
|
||||
}
|
||||
|
||||
// 为每个父级生成3-5个子级
|
||||
const childCount = Math.floor(Math.random() * 3) + 3
|
||||
for (let j = 1; j <= childCount; j++) {
|
||||
const child: any = {
|
||||
seq: `${i}.${j}`,
|
||||
name: `子费用${i}-${j}`,
|
||||
spec: `SPEC${String(i).padStart(3, '0')}-${String(j).padStart(2, '0')}`,
|
||||
cardinal: `子基数${i}-${j}`,
|
||||
__children: []
|
||||
}
|
||||
parent.__children.push(child)
|
||||
}
|
||||
|
||||
data.push(parent)
|
||||
}
|
||||
|
||||
return data
|
||||
})()
|
||||
const handleDialogSuccess = async (formula: string) => {
|
||||
const hot = hstRef.value?.hotInstance
|
||||
if (!hot) return
|
||||
if (currentRowIndex.value == null) return
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns: columns.value,
|
||||
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: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
watch(
|
||||
() => props.height,
|
||||
(newHeight) => {
|
||||
console.log('MarketMaterials newHeight', newHeight)
|
||||
if (newHeight && hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.updateSettings({
|
||||
height: newHeight - 50 - 15// 减去 tabs 头部和 padding 的高度,滚动条
|
||||
const rowData = hot.getSourceDataAtRow(currentRowIndex.value)
|
||||
if (rowData?.id) {
|
||||
try {
|
||||
await updateVariableSetting({
|
||||
id: rowData.id,
|
||||
catalogItemId: catalogItemId.value || '',
|
||||
category: categoryMap[value.value] || 'division',
|
||||
calcBase: { formula }
|
||||
})
|
||||
hstRef.value.hotInstance.render()
|
||||
hot.setDataAtRowProp(currentRowIndex.value, 'calcBase', { formula }, 'calcBaseDialog')
|
||||
hot.setDataAtRowProp(currentRowIndex.value, 'cardinal', formula, 'calcBaseDialog')
|
||||
hot.render()
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('保存计算基数失败')
|
||||
}
|
||||
} else {
|
||||
hot.setDataAtRowProp(currentRowIndex.value, 'calcBase', { formula }, 'calcBaseDialog')
|
||||
hot.setDataAtRowProp(currentRowIndex.value, 'cardinal', formula, 'calcBaseDialog')
|
||||
hot.render()
|
||||
}
|
||||
}
|
||||
|
||||
const value = ref<SegmentedType>('分部分项')
|
||||
|
||||
const options = segmentedOptions
|
||||
|
||||
// 延迟渲染 ElSegmented 以避免初始化时宽度计算问题
|
||||
const isSegmentedReady = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
// 使用 requestAnimationFrame 确保 DOM 已完全渲染
|
||||
requestAnimationFrame(() => {
|
||||
isSegmentedReady.value = true
|
||||
})
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
if (!catalogItemId.value) return
|
||||
|
||||
try {
|
||||
const category = categoryMap[value.value] || 'division'
|
||||
const res = await getVariableSettingList(catalogItemId.value, category)
|
||||
tableData.value = (res || []).map((item: VariableSettingVO, index: number) => ({
|
||||
...item,
|
||||
number: String(index + 1),
|
||||
unit: item.code || '',
|
||||
cardinal: item.calcBase?.formula || '',
|
||||
// 保留 source 字段用于判断只读
|
||||
source: item.source || 'manual'
|
||||
}))
|
||||
|
||||
nextTick(() => {
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.loadData(tableData.value)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载变量设置失败:', error)
|
||||
tableData.value = []
|
||||
nextTick(() => {
|
||||
if (hstRef.value?.hotInstance) {
|
||||
hstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 Segmented 切换动态生成 settings
|
||||
const dynamicSettings = computed(() => {
|
||||
return createDbHstSettings(value.value, {
|
||||
initSchema: (cols: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
cols.forEach((col: ColumnConfig) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
},
|
||||
dialogRendererCallbacks: {
|
||||
onIconClick: (rowData, row) => {
|
||||
openDialog(rowData, row)
|
||||
},
|
||||
},
|
||||
afterChangeCallbacks: {
|
||||
onCreate: async (rowData: any, _row: number) => {
|
||||
if (!rowData.name && !rowData.code) return
|
||||
// 定额取费来源的数据不能创建(理论上不会触发)
|
||||
if (rowData.source === 'fee_item') return
|
||||
try {
|
||||
const id = await createVariableSetting({
|
||||
catalogItemId: catalogItemId.value || '',
|
||||
category: categoryMap[value.value] || 'division',
|
||||
name: rowData.name || '',
|
||||
code: rowData.code || '',
|
||||
calcBase: rowData.calcBase,
|
||||
})
|
||||
rowData.id = id
|
||||
ElMessage.success('创建成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('创建失败')
|
||||
}
|
||||
},
|
||||
onUpdate: async (rowData: any, _row: number) => {
|
||||
if (!rowData.id) return
|
||||
// 定额取费来源的数据不能更新
|
||||
if (rowData.source === 'fee_item') return
|
||||
try {
|
||||
await updateVariableSetting({
|
||||
id: rowData.id,
|
||||
catalogItemId: catalogItemId.value || '',
|
||||
category: categoryMap[value.value] || 'division',
|
||||
name: rowData.name,
|
||||
code: rowData.code,
|
||||
calcBase: rowData.calcBase,
|
||||
})
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 右键菜单
|
||||
const dynamicContextMenuItems = computed(() => [
|
||||
{
|
||||
key: 'insert_row_above',
|
||||
name: '上方插入行',
|
||||
callback: async (arg: any) => {
|
||||
const { selection } = arg || {}
|
||||
const targetRow = selection && selection.length > 0 ? selection[0][0] : 0
|
||||
const referenceNodeId = tableData.value[targetRow]?.id
|
||||
|
||||
try {
|
||||
await createVariableSetting({
|
||||
catalogItemId: catalogItemId.value || '',
|
||||
category: categoryMap[value.value] || 'division',
|
||||
name: '',
|
||||
code: '',
|
||||
referenceNodeId: referenceNodeId,
|
||||
insertPosition: 'above',
|
||||
})
|
||||
await loadData()
|
||||
ElMessage.success('插入成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('插入失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{
|
||||
key: 'insert_row_below',
|
||||
name: '下方插入行',
|
||||
callback: async (arg: any) => {
|
||||
const { selection } = arg || {}
|
||||
const targetRow = selection && selection.length > 0 ? selection[0][0] : tableData.value.length - 1
|
||||
const referenceNodeId = tableData.value[targetRow]?.id
|
||||
|
||||
const value = ref('分部分项???')
|
||||
try {
|
||||
await createVariableSetting({
|
||||
catalogItemId: catalogItemId.value || '',
|
||||
category: categoryMap[value.value] || 'division',
|
||||
name: '',
|
||||
code: '',
|
||||
referenceNodeId: referenceNodeId,
|
||||
insertPosition: 'below',
|
||||
})
|
||||
await loadData()
|
||||
ElMessage.success('插入成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('插入失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'delete_row',
|
||||
name: '删除行',
|
||||
callback: async (arg: any) => {
|
||||
const { selection, hotInstance } = arg || {}
|
||||
if (!selection || selection.length === 0) {
|
||||
ElMessage.warning('请先选中要删除的行')
|
||||
return
|
||||
}
|
||||
|
||||
const options = ['分部分项???', '措施项目???', '其他项目???', '单位汇总???']
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// if (hstRef.value) {
|
||||
// hstRef.value.loadData(mockData)
|
||||
// }
|
||||
}, 100)
|
||||
const row = selection[0][0]
|
||||
const rowData = hotInstance?.getSourceDataAtRow(row) || tableData.value[row]
|
||||
if (!rowData?.id) {
|
||||
ElMessage.warning('该行没有数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 定额取费来源的数据不能删除
|
||||
if (rowData.source === 'fee_item') {
|
||||
ElMessage.warning('定额取费变量不能删除')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteVariableSetting(rowData.id)
|
||||
await loadData()
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 监听类别切换
|
||||
watch(value, () => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 监听 catalogItemId 变化
|
||||
watch(catalogItemId, (val) => {
|
||||
console.log('catalogItemId', val)
|
||||
loadData()
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
|
||||
<div class="w-full h-full flex flex-col">
|
||||
|
||||
<div class="flex flex-col items-start gap-4">
|
||||
<ElSegmented v-model="value" :options="options" size="large" />
|
||||
<ElSegmented v-if="isSegmentedReady" class="variable-settings-segmented" v-model="value" :options="options" size="small" />
|
||||
</div>
|
||||
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
<DbHst ref="hstRef" :settings="dynamicSettings" :contextMenuItems="dynamicContextMenuItems" />
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<ElDialog v-model="dialogVisible" title="费用代码设置" width="60%" :close-on-click-modal="false">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="color: #909399; font-size: 13px; margin-bottom: 8px;">
|
||||
提示: 现在书写的公式只能调用在当前项目以前的取费和系统参数
|
||||
</div>
|
||||
<ElInput
|
||||
v-model="formulaInput"
|
||||
placeholder="请输入公式"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ElTabs v-model="activeTab">
|
||||
<ElTabPane label="全局费用" name="global">
|
||||
<ElTable :data="tabTableData.global" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="分部分项" name="division">
|
||||
<ElTable :data="tabTableData.division" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="措施项目" name="measure">
|
||||
<ElTable :data="tabTableData.measure" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="其他项目" name="other">
|
||||
<ElTable :data="tabTableData.other" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="补充费用" name="supplement">
|
||||
<ElTable :data="tabTableData.supplement" border style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="seq" label="序号" width="80" />
|
||||
<ElTableColumn prop="code" label="费用代号" width="150" />
|
||||
<ElTableColumn prop="name" label="费用名称" min-width="200" />
|
||||
<ElTableColumn prop="value" label="值" width="120" />
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="dialogVisible = false">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<MaterialsDialog
|
||||
v-model="isDialogVisible"
|
||||
:rowData="currentRowData"
|
||||
:catalogItemId="catalogItemId"
|
||||
@success="handleDialogSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.variable-settings-segmented{
|
||||
--el-border-radius-base: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.handsontable .htCalculate {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
font-size: 0;
|
||||
float: right;
|
||||
top: calc((var(--ht-line-height) - var(--ht-icon-size)) / 2);
|
||||
margin-left: calc(var(--ht-gap-size)* 2);
|
||||
margin-right: 1px;
|
||||
}
|
||||
.handsontable .htCalculate::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB0PSIxNzY2MjAwNTkzNTAwIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjgxMzAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTI1MS4yIDM4N0gzMjB2NjguOGMwIDEuOCAxLjggMy4yIDQgMy4yaDQ4YzIuMiAwIDQtMS40IDQtMy4zVjM4N2g2OC44YzEuOCAwIDMuMi0xLjggMy4yLTR2LTQ4YzAtMi4yLTEuNC00LTMuMy00SDM3NnYtNjguOGMwLTEuOC0xLjgtMy4yLTQtMy4yaC00OGMtMi4yIDAtNCAxLjQtNCAzLjJWMzMxaC02OC44Yy0xLjggMC0zLjIgMS44LTMuMiA0djQ4YzAgMi4yIDEuNCA0IDMuMiA0eiBtMzI4IDBoMTkzLjZjMS44IDAgMy4yLTEuOCAzLjItNHYtNDhjMC0yLjItMS40LTQtMy4zLTRINTc5LjJjLTEuOCAwLTMuMiAxLjgtMy4yIDR2NDhjMCAyLjIgMS40IDQgMy4yIDR6IG0wIDI2NWgxOTMuNmMxLjggMCAzLjItMS44IDMuMi00di00OGMwLTIuMi0xLjQtNC0zLjMtNEg1NzkuMmMtMS44IDAtMy4yIDEuOC0zLjIgNHY0OGMwIDIuMiAxLjQgNCAzLjIgNHogbTAgMTA0aDE5My42YzEuOCAwIDMuMi0xLjggMy4yLTR2LTQ4YzAtMi4yLTEuNC00LTMuMy00SDU3OS4yYy0xLjggMC0zLjIgMS44LTMuMiA0djQ4YzAgMi4yIDEuNCA0IDMuMiA0eiBtLTE5NS43LTgxbDYxLjItNzQuOWM0LjMtNS4yIDAuNy0xMy4xLTUuOS0xMy4xSDM4OGMtMi4zIDAtNC41IDEtNS45IDIuOWwtMzQgNDEuNi0zNC00MS42Yy0xLjUtMS44LTMuNy0yLjktNS45LTIuOWgtNTAuOWMtNi42IDAtMTAuMiA3LjktNS45IDEzLjFsNjEuMiA3NC45LTYyLjcgNzYuOGMtNC40IDUuMi0wLjggMTMuMSA1LjggMTMuMWg1MC44YzIuMyAwIDQuNS0xIDUuOS0yLjlsMzUuNS00My41IDM1LjUgNDMuNWMxLjUgMS44IDMuNyAyLjkgNS45IDIuOWg1MC44YzYuNiAwIDEwLjItNy45IDUuOS0xMy4xTDM4My41IDY3NXoiIHAtaWQ9IjgxMzEiPjwvcGF0aD48cGF0aCBkPSJNODgwIDExMkgxNDRjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjczNmMwIDE3LjcgMTQuMyAzMiAzMiAzMmg3MzZjMTcuNyAwIDMyLTE0LjMgMzItMzJWMTQ0YzAtMTcuNy0xNC4zLTMyLTMyLTMyeiBtLTM2IDczMkgxODBWMTgwaDY2NHY2NjR6IiBwLWlkPSI4MTMyIj48L3BhdGg+PC9zdmc+');
|
||||
background-color: currentColor;
|
||||
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
97
apps/web-ele/src/views/database/interface/unit/Workbench.vue
Normal file
97
apps/web-ele/src/views/database/interface/unit/Workbench.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import { DbHst } from '#/components/db-hst'
|
||||
|
||||
const hstRef = ref<any>(null)
|
||||
const columns = ref<any[]>([
|
||||
{ type: 'text', data: 'seq', title: '序', width: 40 },
|
||||
{ type: 'text', data: 'code', title: '字段名称',},
|
||||
{ type: 'checkbox', data: 'category', title: '分部分项隐藏' },
|
||||
{ type: 'checkbox', data: 'name', title: '措施项目隐藏' },
|
||||
{ type: 'checkbox', data: 'feature', title: '其他项目隐葱' },
|
||||
{ type: 'checkbox', data: 'locked', title: '汇总分析隐藏' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
])
|
||||
let rowSchema: any = {}
|
||||
// 根据 columns 的 data 字段生成对象结构
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.data ) {
|
||||
rowSchema[col.data] = null
|
||||
}
|
||||
})
|
||||
|
||||
const mockData = (() => {
|
||||
const data: any[] = []
|
||||
|
||||
// 生成5个父级数据
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const parent = {
|
||||
seq: `${i}`,
|
||||
code: `CODE${String(i).padStart(6, '0')}`,
|
||||
category: false,
|
||||
name: false,
|
||||
feature: false,
|
||||
locked: false,
|
||||
remark: `备注${i}`,
|
||||
level: String(i - 1),
|
||||
__children: [] as any[]
|
||||
}
|
||||
data.push(parent)
|
||||
}
|
||||
|
||||
return data
|
||||
})()
|
||||
|
||||
const settings = {
|
||||
data: mockData,
|
||||
dataSchema: rowSchema,
|
||||
colWidths: 120,
|
||||
columns: columns.value,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
className: 'htCenter htMiddle',
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above:{
|
||||
},
|
||||
row_below: {
|
||||
},
|
||||
separator2: '---------',
|
||||
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)
|
||||
}
|
||||
},
|
||||
},
|
||||
// separator3: '---------',
|
||||
// undo: {},
|
||||
// redo: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// if (hstRef.value) {
|
||||
// hstRef.value.loadData(mockData)
|
||||
// }
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
type CreateSettingsDeps = {
|
||||
formulaInput: Ref<string>
|
||||
}
|
||||
|
||||
const createRowSchema = (columns: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
columns.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const createMockRows = (count: number) => {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const rowText = String(rowNumber).padStart(3, '0')
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
code: `FB${rowText}`,
|
||||
name: `分部分项费用${rowNumber}`,
|
||||
value: String((rowNumber * 10).toFixed(2)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createDivisionSettings = ({ formulaInput }: CreateSettingsDeps) => {
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60, readOnly: true },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 240, readOnly: true },
|
||||
{ type: 'text', data: 'code', title: '代号', width: 160, readOnly: true },
|
||||
]
|
||||
|
||||
const allowedSelectionDataKeys = new Set(['code', 'value'])
|
||||
|
||||
return {
|
||||
data: createMockRows(30),
|
||||
//dataSchema: createRowSchema(columns),
|
||||
colWidths: 150,
|
||||
height: 400,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
selectionMode: 'single',
|
||||
className: 'htCenter',
|
||||
afterSelection(row: number, col: number) {
|
||||
if (row < 0 || col < 0) return
|
||||
const selectedDataKey = columns?.[col]?.data
|
||||
if (!selectedDataKey || !allowedSelectionDataKeys.has(selectedDataKey)) return
|
||||
const cellValue = this.getDataAtCell(row, col)
|
||||
if (cellValue == null) return
|
||||
formulaInput.value += String(cellValue)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export { createDivisionSettings } from './division'
|
||||
export { createMeasureSettings } from './measure'
|
||||
export { createOtherSettings } from './other'
|
||||
export { createUnitSummarySettings } from './summary'
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
type CreateSettingsDeps = {
|
||||
formulaInput: Ref<string>
|
||||
}
|
||||
|
||||
const createRowSchema = (columns: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
columns.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const createMockRows = (count: number) => {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const rowText = String(rowNumber).padStart(3, '0')
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
code: `CS${rowText}`,
|
||||
name: `措施费${rowNumber}`,
|
||||
value: String((rowNumber * 12).toFixed(2)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createMeasureSettings = ({ formulaInput }: CreateSettingsDeps) => {
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60, readOnly: true },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 240, readOnly: true },
|
||||
{ type: 'text', data: 'code', title: '代号', width: 160, readOnly: true },
|
||||
]
|
||||
|
||||
const allowedSelectionDataKeys = new Set(['code', 'value'])
|
||||
|
||||
return {
|
||||
data: createMockRows(30),
|
||||
//dataSchema: createRowSchema(columns),
|
||||
colWidths: 150,
|
||||
height: 400,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
selectionMode: 'single',
|
||||
className: 'htCenter',
|
||||
afterSelection(row: number, col: number) {
|
||||
if (row < 0 || col < 0) return
|
||||
const selectedDataKey = columns?.[col]?.data
|
||||
if (!selectedDataKey || !allowedSelectionDataKeys.has(selectedDataKey)) return
|
||||
const cellValue = this.getDataAtCell(row, col)
|
||||
if (cellValue == null) return
|
||||
formulaInput.value += String(cellValue)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
type CreateSettingsDeps = {
|
||||
formulaInput: Ref<string>
|
||||
}
|
||||
|
||||
const createRowSchema = (columns: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
columns.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const createMockRows = (count: number) => {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const rowText = String(rowNumber).padStart(3, '0')
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
code: `QT${rowText}`,
|
||||
name: `其他项目费用${rowNumber}`,
|
||||
value: String((rowNumber * 8).toFixed(2)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createOtherSettings = ({ formulaInput }: CreateSettingsDeps) => {
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60, readOnly: true },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 240, readOnly: true },
|
||||
{ type: 'text', data: 'code', title: '代号', width: 160, readOnly: true },
|
||||
]
|
||||
|
||||
const allowedSelectionDataKeys = new Set(['code', 'value'])
|
||||
|
||||
return {
|
||||
data: createMockRows(30),
|
||||
//dataSchema: createRowSchema(columns),
|
||||
colWidths: 150,
|
||||
height: 400,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
selectionMode: 'single',
|
||||
className: 'htCenter',
|
||||
afterSelection(row: number, col: number) {
|
||||
if (row < 0 || col < 0) return
|
||||
const selectedDataKey = columns?.[col]?.data
|
||||
if (!selectedDataKey || !allowedSelectionDataKeys.has(selectedDataKey)) return
|
||||
const cellValue = this.getDataAtCell(row, col)
|
||||
if (cellValue == null) return
|
||||
formulaInput.value += String(cellValue)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type ColumnConfig = {
|
||||
type: 'text'
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
type CreateSettingsDeps = {
|
||||
formulaInput: Ref<string>
|
||||
}
|
||||
|
||||
const createRowSchema = (columns: ColumnConfig[]) => {
|
||||
const schema: Record<string, null> = {}
|
||||
columns.forEach((col) => {
|
||||
if (col.data) schema[col.data] = null
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
const createMockRows = (count: number) => {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const rowNumber = index + 1
|
||||
const rowText = String(rowNumber).padStart(3, '0')
|
||||
return {
|
||||
seq: String(rowNumber),
|
||||
code: `DW${rowText}`,
|
||||
name: `单位汇总费用${rowNumber}`,
|
||||
value: String((rowNumber * 6).toFixed(2)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createUnitSummarySettings = ({ formulaInput }: CreateSettingsDeps) => {
|
||||
const columns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'seq', title: '序号', width: 60, readOnly: true },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 240, readOnly: true },
|
||||
{ type: 'text', data: 'code', title: '代号', width: 160, readOnly: true },
|
||||
]
|
||||
|
||||
const allowedSelectionDataKeys = new Set(['code', 'value'])
|
||||
|
||||
return {
|
||||
data: createMockRows(30),
|
||||
//dataSchema: createRowSchema(columns),
|
||||
colWidths: 150,
|
||||
height: 400,
|
||||
columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
selectionMode: 'single',
|
||||
className: 'htCenter',
|
||||
afterSelection(row: number, col: number) {
|
||||
if (row < 0 || col < 0) return
|
||||
const selectedDataKey = columns?.[col]?.data
|
||||
if (!selectedDataKey || !allowedSelectionDataKeys.has(selectedDataKey)) return
|
||||
const cellValue = this.getDataAtCell(row, col)
|
||||
if (cellValue == null) return
|
||||
formulaInput.value += String(cellValue)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* ElSegmented 切换对应的 DbHst 组件 settings 配置
|
||||
* 分部分项 | 措施项目 | 其他项目 | 单位汇总
|
||||
*/
|
||||
import Handsontable from 'handsontable';
|
||||
// Segmented 选项类型
|
||||
export type SegmentedType = '分部分项' | '措施项目' | '其他项目' | '单位汇总'
|
||||
|
||||
// Segmented 选项列表
|
||||
export const segmentedOptions: SegmentedType[] = ['分部分项', '措施项目', '其他项目', '单位汇总']
|
||||
|
||||
// 列配置类型
|
||||
export interface ColumnConfig {
|
||||
type: string
|
||||
data: string
|
||||
title: string
|
||||
width?: number
|
||||
renderer?: any
|
||||
required?: boolean
|
||||
autoWidth?: boolean
|
||||
readOnly?: boolean
|
||||
code?: boolean
|
||||
}
|
||||
|
||||
// Settings 配置类型
|
||||
export interface DbHstSettings {
|
||||
data: any[]
|
||||
dataSchema?: any
|
||||
colWidths?: number | number[]
|
||||
columns: ColumnConfig[]
|
||||
rowHeaders?: boolean
|
||||
nestedRows?: boolean
|
||||
bindRowsWithHeaders?: boolean
|
||||
contextMenu?: any
|
||||
afterChange?: (changes: any, source: any) => void
|
||||
}
|
||||
|
||||
// ContextMenuItem 类型
|
||||
export interface ContextMenuItem {
|
||||
key: string
|
||||
name: string
|
||||
callback?: (hotInstance: any) => void
|
||||
separator?: boolean
|
||||
}
|
||||
|
||||
export type DialogRendererCallbacks = {
|
||||
onIconClick?: (rowData: any, row: number) => void
|
||||
}
|
||||
const { EventManager } = Handsontable
|
||||
const { addClass, hasClass } = Handsontable.dom
|
||||
|
||||
export const createDialogRenderer = (callbacks?: DialogRendererCallbacks) => {
|
||||
return (instance: any, TD: HTMLTableCellElement, row: number, col: number, prop: string, value: any, cellProperties: any) => {
|
||||
const { rootDocument } = instance;
|
||||
|
||||
const ICON = rootDocument.createElement('DIV');
|
||||
|
||||
ICON.className = 'htCalculate';//htAutocompleteArrow
|
||||
|
||||
ICON.appendChild(rootDocument.createTextNode(String.fromCharCode(9660)));
|
||||
|
||||
Handsontable.renderers.TextRenderer.apply(this, [instance, TD, row, col, prop, value, cellProperties]);
|
||||
|
||||
if (!TD.firstChild) { // http://jsperf.com/empty-node-if-needed
|
||||
// otherwise empty fields appear borderless in demo/renderers.html (IE)
|
||||
TD.appendChild(rootDocument.createTextNode(String.fromCharCode(160))); // workaround for https://github.com/handsontable/handsontable/issues/1946
|
||||
// this is faster than innerHTML. See: https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips
|
||||
}
|
||||
|
||||
TD.insertBefore(ICON, TD.firstChild);
|
||||
|
||||
// addClass(TD, 'htAutocomplete');
|
||||
addClass(ICON, ['!cursor-pointer'])
|
||||
if (!instance.acArrowListener) {
|
||||
const eventManager = new EventManager(instance);
|
||||
|
||||
instance.acArrowListener = function (event: MouseEvent) {
|
||||
if (hasClass(event.target as HTMLElement, 'htCalculate')) {
|
||||
const targetCell = (event.target as HTMLElement).closest('td')
|
||||
|
||||
if (targetCell) {
|
||||
const coords = instance.getCoords(targetCell)
|
||||
const cellMeta = instance.getCellMeta(coords.row, coords.col)//cellProperties
|
||||
const cellValue = instance.getDataAtCell(coords.row, coords.col)
|
||||
const cellProp = instance.colToProp(coords.col) // 正确获取当前单元格的 prop
|
||||
let cellSource = (cellMeta as any).source || []
|
||||
// const cellCallback = (cellMeta as { callback?: ChangeCallback }).callback
|
||||
// console.log('click arrow', coords, cellMeta, cellValue, cellSource)
|
||||
//activeContext.value = {instance, TD, row:coords.row, column:coords.col, prop:cellProp, value:cellValue, cellProperties:cellMeta}
|
||||
const rowData = instance.getSourceDataAtRow?.(coords.row)
|
||||
callbacks?.onIconClick?.(rowData, coords.row)
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
eventManager.addEventListener(instance.rootElement, 'mousedown', instance.acArrowListener);
|
||||
|
||||
// We need to unbind the listener after the table has been destroyed
|
||||
instance.addHookOnce('afterDestroy', () => {
|
||||
eventManager.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const dialogRenderer = createDialogRenderer()
|
||||
// 分部分项 - 列配置
|
||||
export const fenbufenxiangColumns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'number', title: '序号', width: 80 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 200 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', width: 80 },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数', width: 120 },
|
||||
]
|
||||
|
||||
// 措施项目 - 列配置
|
||||
export const cuoshixiangmuColumns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'number', title: '序号', width: 80 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 200 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', width: 80 },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数', width: 120 },
|
||||
]
|
||||
|
||||
// 其他项目 - 列配置
|
||||
export const qitaxiangmuColumns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'number', title: '序号', width: 80 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 200 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', width: 80 },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数', width: 120 },
|
||||
]
|
||||
|
||||
// 单位汇总 - 列配置
|
||||
export const danweihuizongColumns: ColumnConfig[] = [
|
||||
{ type: 'text', data: 'number', title: '序号', width: 80 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', width: 200 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', width: 80 },
|
||||
{ type: 'text', data: 'cardinal', title: '计算基数', width: 120 },
|
||||
]
|
||||
|
||||
// 根据类型获取列配置
|
||||
export const getColumnsByType = (type: SegmentedType): ColumnConfig[] => {
|
||||
const columnsMap: Record<SegmentedType, ColumnConfig[]> = {
|
||||
'分部分项': fenbufenxiangColumns,
|
||||
'措施项目': cuoshixiangmuColumns,
|
||||
'其他项目': qitaxiangmuColumns,
|
||||
'单位汇总': danweihuizongColumns,
|
||||
}
|
||||
return columnsMap[type] || fenbufenxiangColumns
|
||||
}
|
||||
|
||||
// ===================== 独立的 contextMenu 配置 =====================
|
||||
|
||||
// 分部分项 - contextMenu
|
||||
export const createFenbufenxiangContextMenu = () => ({
|
||||
items: {
|
||||
row_above: { name: '在上方插入行' },
|
||||
row_below: { name: '在下方插入行' },
|
||||
separator1: '---------',
|
||||
remove_row: {
|
||||
name: '删除行',
|
||||
callback: function (this: 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 措施项目 - contextMenu
|
||||
export const createCuoshixiangmuContextMenu = () => ({
|
||||
items: {
|
||||
row_above: { name: '在上方插入措施项' },
|
||||
row_below: { name: '在下方插入措施项' },
|
||||
separator1: '---------',
|
||||
remove_row: {
|
||||
name: '删除措施项',
|
||||
callback: function (this: 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 其他项目 - contextMenu
|
||||
export const createQitaxiangmuContextMenu = () => ({
|
||||
items: {
|
||||
row_above: { name: '在上方插入项目' },
|
||||
row_below: { name: '在下方插入项目' },
|
||||
separator1: '---------',
|
||||
remove_row: {
|
||||
name: '删除项目',
|
||||
callback: function (this: 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 单位汇总 - contextMenu (只读,无编辑操作)
|
||||
export const createDanweihuizongContextMenu = () => ({
|
||||
items: {}
|
||||
})
|
||||
|
||||
// 根据类型获取 contextMenu
|
||||
export const getContextMenuByType = (type: SegmentedType) => {
|
||||
const menuMap: Record<SegmentedType, () => any> = {
|
||||
'分部分项': createFenbufenxiangContextMenu,
|
||||
'措施项目': createCuoshixiangmuContextMenu,
|
||||
'其他项目': createQitaxiangmuContextMenu,
|
||||
'单位汇总': createDanweihuizongContextMenu,
|
||||
}
|
||||
return menuMap[type]?.()
|
||||
}
|
||||
|
||||
// ===================== 独立的 afterChange 配置 =====================
|
||||
|
||||
// 分部分项 - afterChange
|
||||
export const createFenbufenxiangAfterChange = (callbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}) => {
|
||||
return function (this: any, changes: any, source: any) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'calcBaseDialog') return
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
console.log('分部分项 afterChange:', rowData)
|
||||
if (rowData?.id == null) {
|
||||
callbacks?.onCreate?.(rowData, row)
|
||||
} else {
|
||||
callbacks?.onUpdate?.(rowData, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 措施项目 - afterChange
|
||||
export const createCuoshixiangmuAfterChange = (callbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}) => {
|
||||
return function (this: any, changes: any, source: any) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'calcBaseDialog') return
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
console.log('措施项目 afterChange:', rowData)
|
||||
if (rowData?.id == null) {
|
||||
callbacks?.onCreate?.(rowData, row)
|
||||
} else {
|
||||
callbacks?.onUpdate?.(rowData, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他项目 - afterChange
|
||||
export const createQitaxiangmuAfterChange = (callbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}) => {
|
||||
return function (this: any, changes: any, source: any) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'calcBaseDialog') return
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
console.log('其他项目 afterChange:', rowData)
|
||||
if (rowData?.id == null) {
|
||||
callbacks?.onCreate?.(rowData, row)
|
||||
} else {
|
||||
callbacks?.onUpdate?.(rowData, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单位汇总 - afterChange (只读,不处理变更)
|
||||
export const createDanweihuizongAfterChange = (
|
||||
callbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}
|
||||
) => {
|
||||
return function (this: any, changes: any, source: any) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'calcBaseDialog') return
|
||||
const row = changes[0][0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
console.log('单位汇总 afterChange:', rowData)
|
||||
if (rowData?.id == null) {
|
||||
callbacks?.onCreate?.(rowData, row)
|
||||
} else {
|
||||
callbacks?.onUpdate?.(rowData, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据类型获取 afterChange
|
||||
export const getAfterChangeByType = (
|
||||
type: SegmentedType,
|
||||
callbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}
|
||||
) => {
|
||||
const afterChangeMap: Record<SegmentedType, () => any> = {
|
||||
'分部分项': () => createFenbufenxiangAfterChange(callbacks),
|
||||
'措施项目': () => createCuoshixiangmuAfterChange(callbacks),
|
||||
'其他项目': () => createQitaxiangmuAfterChange(callbacks),
|
||||
'单位汇总': () => createDanweihuizongAfterChange(callbacks),
|
||||
}
|
||||
return afterChangeMap[type]?.()
|
||||
}
|
||||
|
||||
// ===================== 创建 DbHst settings 的工厂函数 =====================
|
||||
|
||||
export const createDbHstSettings = (
|
||||
type: SegmentedType,
|
||||
options?: {
|
||||
initSchema?: (columns: ColumnConfig[]) => any
|
||||
dialogRendererCallbacks?: DialogRendererCallbacks
|
||||
afterChangeCallbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}
|
||||
}
|
||||
): DbHstSettings => {
|
||||
const columns = getColumnsByType(type).map((col) => {
|
||||
if (col.data !== 'cardinal') return col
|
||||
if (!options?.dialogRendererCallbacks?.onIconClick) return col
|
||||
return {
|
||||
...col,
|
||||
renderer: createDialogRenderer(options.dialogRendererCallbacks),
|
||||
}
|
||||
})
|
||||
|
||||
const settings: DbHstSettings = {
|
||||
data: [],
|
||||
// dataSchema: options?.initSchema?.(columns),
|
||||
colWidths: 150,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
contextMenu: false, // 禁用原生 contextMenu,使用 DbHst 组件的 contextMenuItems
|
||||
afterChange: getAfterChangeByType(type, options?.afterChangeCallbacks),
|
||||
// 根据 source 字段设置单元格只读
|
||||
cells(row: number, _col: number) {
|
||||
const cellProperties: any = {}
|
||||
const rowData = this.instance?.getSourceDataAtRow(row)
|
||||
// 定额取费来源的数据设置为只读
|
||||
if (rowData?.source === 'fee_item') {
|
||||
cellProperties.readOnly = true
|
||||
cellProperties.className = 'htDimmed' // 灰色背景表示只读
|
||||
}
|
||||
return cellProperties
|
||||
},
|
||||
} as any
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
// 获取所有类型的 settings 映射
|
||||
export const getAllSettings = (
|
||||
options?: {
|
||||
initSchema?: (columns: ColumnConfig[]) => any
|
||||
dialogRendererCallbacks?: DialogRendererCallbacks
|
||||
afterChangeCallbacks?: {
|
||||
onCreate?: (rowData: any, row: number) => void
|
||||
onUpdate?: (rowData: any, row: number) => void
|
||||
}
|
||||
}
|
||||
): Record<SegmentedType, DbHstSettings> => {
|
||||
return {
|
||||
'分部分项': createDbHstSettings('分部分项', options),
|
||||
'措施项目': createDbHstSettings('措施项目', options),
|
||||
'其他项目': createDbHstSettings('其他项目', options),
|
||||
'单位汇总': createDbHstSettings('单位汇总', options),
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== ContextMenuItems 配置 =====================
|
||||
|
||||
// 分部分项 - contextMenuItems
|
||||
export const createFenbufenxiangContextMenuItems = (
|
||||
options?: {
|
||||
onAfterAdd?: (hotInstance: any) => void
|
||||
}
|
||||
): ContextMenuItem[] => [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// // 等待 DOM 更新后重新渲染以应用验证样式
|
||||
// nextTick(() => {
|
||||
// hotInstance.render()
|
||||
// })
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 措施项目 - contextMenuItems
|
||||
export const createCuoshixiangmuContextMenuItems = (
|
||||
options?: {
|
||||
onAfterAdd?: (hotInstance: any) => void
|
||||
}
|
||||
): ContextMenuItem[] => [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// // 等待 DOM 更新后重新渲染以应用验证样式
|
||||
// nextTick(() => {
|
||||
// hotInstance.render()
|
||||
// })
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 其他项目 - contextMenuItems
|
||||
export const createQitaxiangmuContextMenuItems = (
|
||||
options?: {
|
||||
onAfterAdd?: (hotInstance: any) => void
|
||||
}
|
||||
): ContextMenuItem[] => [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// // 等待 DOM 更新后重新渲染以应用验证样式
|
||||
// nextTick(() => {
|
||||
// hotInstance.render()
|
||||
// })
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 单位汇总 - contextMenuItems (只读,不提供编辑操作)
|
||||
export const createDanweihuizongContextMenuItems = (options?: {
|
||||
onAfterAdd?: (hotInstance: any) => void
|
||||
}): ContextMenuItem[] => [
|
||||
{
|
||||
key: 'row_above',
|
||||
name: '新增行',
|
||||
callback: (hotInstance: any) => {
|
||||
// 执行新增行操作
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
// // 等待 DOM 更新后重新渲染以应用验证样式
|
||||
// nextTick(() => {
|
||||
// hotInstance.render()
|
||||
// })
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 根据类型获取 contextMenuItems
|
||||
export const getContextMenuItemsByType = (
|
||||
type: SegmentedType,
|
||||
options?: {
|
||||
onAfterAdd?: (hotInstance: any) => void
|
||||
}
|
||||
): ContextMenuItem[] => {
|
||||
const menuItemsMap: Record<SegmentedType, () => ContextMenuItem[]> = {
|
||||
'分部分项': () => createFenbufenxiangContextMenuItems(options),
|
||||
'措施项目': () => createCuoshixiangmuContextMenuItems(options),
|
||||
'其他项目': () => createQitaxiangmuContextMenuItems(options),
|
||||
'单位汇总': () => createDanweihuizongContextMenuItems(options),
|
||||
}
|
||||
return menuItemsMap[type]?.() || []
|
||||
}
|
||||
|
||||
// ===================== 模拟数据生成 =====================
|
||||
|
||||
const generateMockData = (prefix: string, count: number = 5) => {
|
||||
const data: any[] = []
|
||||
for (let i = 1; i <= count; i++) {
|
||||
data.push({
|
||||
number: `${i}`,
|
||||
name: `${prefix}${i}`,
|
||||
unit: `UNIT${String(i).padStart(3, '0')}`,
|
||||
cardinal: `基数${i}`,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export const fenbufenxiangMockData = generateMockData('分部分项', 5)
|
||||
|
||||
export const cuoshixiangmuMockData = generateMockData('措施项目', 4)
|
||||
|
||||
export const qitaxiangmuMockData = generateMockData('其他项目', 3)
|
||||
|
||||
export const danweihuizongMockData = generateMockData('单位汇总', 6)
|
||||
|
||||
export const getMockDataByType = (type: SegmentedType): any[] => {
|
||||
const mockDataMap: Record<SegmentedType, any[]> = {
|
||||
'分部分项': fenbufenxiangMockData,
|
||||
'措施项目': cuoshixiangmuMockData,
|
||||
'其他项目': qitaxiangmuMockData,
|
||||
'单位汇总': danweihuizongMockData,
|
||||
}
|
||||
return mockDataMap[type] || []
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { getRowData, getCellValueByProp } from '#/components/db-hst/command'
|
||||
|
||||
const { EventManager } = Handsontable
|
||||
const { addClass, hasClass } = Handsontable.dom
|
||||
|
||||
//已定位问题根因:自定义 renderer 在隐藏列后因 DOM 复用产生脏渲染,不是单纯没刷新整表。
|
||||
// baseNumber renderer- 只在清单节点显示图标
|
||||
export const baseNumberRenderer = (
|
||||
instance: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: string | number,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
// 先重绘 TD,防止 DOM 复用时残留旧内容
|
||||
Handsontable.renderers.TextRenderer.apply(this, [instance, TD, row, col, prop, value, cellProperties])
|
||||
|
||||
const rowData = getRowData(instance, row)
|
||||
const isBoqNode = rowData?.nodeType === 'boq'
|
||||
if (!isBoqNode) return
|
||||
|
||||
const { rootDocument } = instance
|
||||
const ICON = rootDocument.createElement('DIV')
|
||||
|
||||
ICON.className = 'htMenuLine'
|
||||
ICON.appendChild(rootDocument.createTextNode(String.fromCharCode(9660)))
|
||||
|
||||
if (!TD.firstChild) { TD.appendChild(rootDocument.createTextNode(String.fromCharCode(160))) }
|
||||
const targetIcon = 'htBaseNumberIcon'
|
||||
TD.insertBefore(ICON, TD.firstChild)
|
||||
addClass(ICON, ['!cursor-pointer', targetIcon])
|
||||
|
||||
if (!instance[targetIcon + 'Listener']) {
|
||||
const eventManager = new EventManager(instance)
|
||||
instance[targetIcon + 'Listener'] = function (event: MouseEvent) {
|
||||
|
||||
if (hasClass(event.target as HTMLElement, targetIcon)) {
|
||||
const targetCell = (event.target as HTMLElement).closest('td')
|
||||
|
||||
if (targetCell) {
|
||||
const coords = instance.getCoords(targetCell)
|
||||
const cellMeta = instance.getCellMeta(coords.row, coords.col)
|
||||
if (cellMeta.readOnly != undefined && cellMeta.readOnly) return
|
||||
if (cellMeta.editor != undefined && !cellMeta.editor) return
|
||||
const { cellProp, cellValue, rowData } = getCellValueByProp(instance, coords.row, coords.col)
|
||||
//执行自定义方法
|
||||
if (instance.__onBaseNumber) {
|
||||
instance.__onBaseNumber({
|
||||
instance,
|
||||
TD,
|
||||
row: coords.row,
|
||||
column: coords.col,
|
||||
prop: cellProp,
|
||||
value: cellValue,
|
||||
cellProperties: cellMeta,
|
||||
rowData,
|
||||
})
|
||||
}
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
eventManager.addEventListener(instance.rootElement, 'mousedown', instance[targetIcon + 'Listener'], true)
|
||||
|
||||
instance.addHookOnce('afterDestroy', () => {
|
||||
eventManager.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
// 验证公式(参考后台 QuotaVariableSettingServiceImpl.validateCalcBase)
|
||||
export function validateFormula(formulaStr: string): { valid: boolean; error?: string } {
|
||||
if (!formulaStr || formulaStr.trim() === '') {
|
||||
return { valid: true } // 空公式是允许的
|
||||
}
|
||||
|
||||
const trimmedFormula = formulaStr.trim()
|
||||
|
||||
// 验证公式语法:只允许字母、数字、运算符和括号
|
||||
const validPattern = /^[A-Za-z0-9_+\-*/().\s]+$/
|
||||
if (!validPattern.test(trimmedFormula)) {
|
||||
return { valid: false, error: '公式只能包含字母、数字、运算符(+-*/)和括号' }
|
||||
}
|
||||
|
||||
// 验证括号匹配
|
||||
let parenCount = 0
|
||||
for (const char of trimmedFormula) {
|
||||
if (char === '(') parenCount++
|
||||
if (char === ')') parenCount--
|
||||
if (parenCount < 0) {
|
||||
return { valid: false, error: '括号不匹配' }
|
||||
}
|
||||
}
|
||||
if (parenCount !== 0) {
|
||||
return { valid: false, error: '括号不匹配' }
|
||||
}
|
||||
|
||||
// 验证公式不能以运算符或小数点结尾
|
||||
if (/[+\-*/.]$/.test(trimmedFormula)) {
|
||||
return { valid: false, error: '公式不能以运算符或小数点结尾' }
|
||||
}
|
||||
|
||||
// 验证公式不能以运算符或小数点开头(除了负号)
|
||||
if (/^[+*/.]/.test(trimmedFormula)) {
|
||||
return { valid: false, error: '公式不能以运算符或小数点开头' }
|
||||
}
|
||||
|
||||
// 验证不能有连续的运算符
|
||||
if (/[+\-*/]{2,}/.test(trimmedFormula)) {
|
||||
return { valid: false, error: '公式不能有连续的运算符' }
|
||||
}
|
||||
|
||||
// 验证小数点格式
|
||||
if (/\.{2,}/.test(trimmedFormula)) {
|
||||
return { valid: false, error: '小数点格式不正确' }
|
||||
}
|
||||
|
||||
// 验证小数点后面不能直接跟运算符或括号
|
||||
if (/\.[+\-*/()]/.test(trimmedFormula)) {
|
||||
return { valid: false, error: '小数点后面必须跟数字' }
|
||||
}
|
||||
|
||||
// 注意:变量有效性检查已移至后端,前端只进行基础语法验证
|
||||
return { valid: true }
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import { getVariableSettingList, getVariableSettingListByCompileTree, type VariableSettingVO } from '#/api/database/quota/variableSetting';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog, ElInput, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { validateFormula } from '.';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
projectId?: string | number
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'success', data: any): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// 内部状态
|
||||
const visible = ref(false)
|
||||
const compileTreeId = ref<string | number>('')
|
||||
const division = ref<any>(null)
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeKey = ref('division')
|
||||
// 公式输入框
|
||||
const formula = ref('')
|
||||
const hstRef = ref<any>()
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const guideColumns = [
|
||||
{ type: 'numeric', data: 'sortOrder', title: '序号', editor: false, width: 50 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', editor: false, width: 150 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', editor: false, width: 200 },
|
||||
{ type: 'text', data: 'summaryValue', title: '汇总值', editor: false, width: 150 },
|
||||
]
|
||||
|
||||
const settings = {
|
||||
data: [],
|
||||
colWidths: 150,
|
||||
columns: guideColumns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(this: any, event: MouseEvent, coords: any, _TD: HTMLTableCellElement) {
|
||||
if (event.detail === 1 && event.button === 0) {
|
||||
const rowData = this.getSourceDataAtRow(coords.row)
|
||||
if (rowData?.code) {
|
||||
formula.value += String(rowData.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 确定按钮
|
||||
function handleConfirm() {
|
||||
const validation = validateFormula(formula.value)
|
||||
if (!validation.valid) {
|
||||
ElMessage.error(validation.error || '公式验证失败')
|
||||
return
|
||||
}
|
||||
// loading.value = true
|
||||
// loading.value = false
|
||||
division.value.instance.setDataAtRowProp([
|
||||
[division.value.row, 'baseNumber', formula.value || ''],
|
||||
[division.value.row, 'attributes', {
|
||||
...division.value.rowData.attributes,
|
||||
baseNumber: {
|
||||
formula: formula.value || ''
|
||||
}
|
||||
}]
|
||||
], 'update')
|
||||
emit('success', { formula: formula.value })
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 显示弹窗(暴露给父组件调用)
|
||||
function show(selectedNode: any, params: any) {
|
||||
console.log('show-baseNumber', selectedNode, params)
|
||||
compileTreeId.value = selectedNode.parentId//获取定额专业ID
|
||||
division.value = params
|
||||
// formula.value = params.rowData?.attributes?.baseNumber?.formula || ''
|
||||
visible.value = true
|
||||
loadVariableSettings()
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ show, close })
|
||||
|
||||
// 加载变量设置(一次性获取所有定额专业的数据)
|
||||
async function loadVariableSettings() {
|
||||
if (!compileTreeId.value) {
|
||||
console.warn('compileTreeId is required')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const divisionRowData = division.value.rowData
|
||||
formula.value = divisionRowData.attributes?.baseNumber?.formula || ''
|
||||
// 获取基数范围(从当前行的 attributes.baseNumberRange.selectedIds 获取)
|
||||
// const baseNumberRangeIds = divisionRowData?.attributes?.baseNumberRange?.selectedIds || []
|
||||
|
||||
// 一次性获取所有变量设置(后端自动合并所有定额专业)
|
||||
const data = await getVariableSettingList(String(compileTreeId.value), activeKey.value)
|
||||
|
||||
console.log('data', data)
|
||||
const tableData = data.map((item, index) => ({
|
||||
...item,
|
||||
sortOrder: index + 1,
|
||||
}))
|
||||
|
||||
setTimeout(() => {
|
||||
hstRef.value.hotInstance.loadData(tableData)
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error('加载变量设置失败:', error)
|
||||
ElMessage.error('加载变量设置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听标签页切换
|
||||
watch(() => activeKey.value, (val) => {
|
||||
// console.log('activeKey.value',val)
|
||||
nextTick(() => {
|
||||
// refreshTableData()
|
||||
loadVariableSettings()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog :model-value="visible" destroy-on-close draggable @update:model-value="(val: boolean) => visible = val"
|
||||
@closed="close" title="基数设置" width="60%">
|
||||
<div class="flex flex-col h-[500px]">
|
||||
<div><span class="text-red-500">*</span>提示: 点击表格中的费用代号可添加到公式中</div>
|
||||
<ElInput type="text" size="large" placeholder="输入公式" v-model="formula" />
|
||||
|
||||
<!-- 类别标签页 -->
|
||||
<ElTabs v-model="activeKey">
|
||||
<ElTabPane label="分部分项" name="division" />
|
||||
<ElTabPane label="措施项目" name="measure" />
|
||||
<ElTabPane label="其他项目" name="other" />
|
||||
<ElTabPane label="单位汇总" name="unit_summary" />
|
||||
</ElTabs>
|
||||
|
||||
<div class="flex-1 overflow-hidden pt-[1px]">
|
||||
<DbHst ref="hstRef" :settings="settings" v-show="visible" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm" :loading="loading">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,70 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { getRowData, getCellValueByProp } from '#/components/db-hst/command'
|
||||
|
||||
const { EventManager } = Handsontable
|
||||
const { addClass, hasClass } = Handsontable.dom
|
||||
|
||||
// 基数范围渲染器(参考统一取费的范围字段)- 只在清单节点显示图标
|
||||
export const baseNumberRangeRenderer = (
|
||||
instance: any,
|
||||
TD: HTMLTableCellElement,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: string | number,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
// 先重绘 TD,防止 DOM 复用时残留旧内容
|
||||
Handsontable.renderers.TextRenderer.apply(this, [instance, TD, row, col, prop, value, cellProperties])
|
||||
|
||||
const rowData = getRowData(instance, row)
|
||||
const isBoqNode = rowData?.nodeType === 'boq'
|
||||
if (!isBoqNode) return
|
||||
|
||||
const { rootDocument } = instance
|
||||
const ICON = rootDocument.createElement('DIV')
|
||||
|
||||
ICON.className = 'htMenuLine'
|
||||
ICON.appendChild(rootDocument.createTextNode(String.fromCharCode(9660)))
|
||||
|
||||
if (!TD.firstChild) { TD.appendChild(rootDocument.createTextNode(String.fromCharCode(160))) }
|
||||
const targetIcon = 'htBaseNumberRangeIcon'
|
||||
TD.insertBefore(ICON, TD.firstChild)
|
||||
addClass(ICON, ['!cursor-pointer',targetIcon])
|
||||
|
||||
if (!instance[targetIcon + 'Listener']) {
|
||||
const eventManager = new EventManager(instance)
|
||||
instance[targetIcon + 'Listener'] = function (event: MouseEvent) {
|
||||
if (hasClass(event.target as HTMLElement, targetIcon)) {
|
||||
const targetCell = (event.target as HTMLElement).closest('td')
|
||||
|
||||
if (targetCell) {
|
||||
const coords = instance.getCoords(targetCell)
|
||||
const cellMeta = instance.getCellMeta(coords.row, coords.col)
|
||||
if (cellMeta.readOnly != undefined && cellMeta.readOnly) return
|
||||
if (cellMeta.editor != undefined && !cellMeta.editor) return
|
||||
const { cellProp, cellValue, rowData } = getCellValueByProp(instance, coords.row, coords.col)
|
||||
if (instance.__onBaseNumberRange) {
|
||||
instance.__onBaseNumberRange({
|
||||
instance,
|
||||
TD,
|
||||
row: coords.row,
|
||||
column: coords.col,
|
||||
prop: cellProp,
|
||||
value: cellValue,
|
||||
cellProperties: cellMeta,
|
||||
rowData,
|
||||
})
|
||||
}
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
eventManager.addEventListener(instance.rootElement, 'mousedown', instance[targetIcon + 'Listener'], true)
|
||||
|
||||
instance.addHookOnce('afterDestroy', () => {
|
||||
eventManager.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<script lang="ts" setup>
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { hstCheckboxChange } from '#/components/db-hst/checkbox';
|
||||
import { ElButton, ElDialog, ElInput, ElMessage, ElTabPane, ElTabs, ElSegmented } from 'element-plus';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { getDivisionTemplateTree } from '#/api/database/interface/config';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'success', data: any): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// 内部状态
|
||||
const visible = ref(false)
|
||||
|
||||
const hstRef = ref()
|
||||
const catalogItemId = ref()
|
||||
const division = ref()
|
||||
const selectedIdsMap = ref<Record<string, string[]>>({
|
||||
division: [],
|
||||
measure: [],
|
||||
})
|
||||
|
||||
const segmentedValue = ref('division')
|
||||
const segmentedOptions = [
|
||||
{ label: '分部分项', value: 'division' },
|
||||
{ label: '措施项目', value: 'measure' },
|
||||
]
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{ type: 'db.nestedRows', data: 'sortOrder', title: '序号', editor: false, width: 50 },
|
||||
{ type: 'text', data: 'code', title: '费用代号', editor: false, width: 150 },
|
||||
{ type: 'text', data: 'name', title: '费用名称', editor: false, width: 200 },
|
||||
{ type: 'text', data: 'summaryValue', title: '汇总值', editor: false, width: 150 },
|
||||
{ type: 'checkbox', data: 'selected', title: '选择', className: 'htCenter', width: '60' },
|
||||
]
|
||||
|
||||
const settings = {
|
||||
data: [],
|
||||
colWidths: 150,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
// beforeOnCellMouseDown(this: any, event: MouseEvent, coords: any, _TD: HTMLTableCellElement) {
|
||||
// if (event.detail === 1 && event.button === 0) {
|
||||
// const rowData = this.getSourceDataAtRow(coords.row)
|
||||
|
||||
// }
|
||||
// },
|
||||
afterChange: hstCheckboxChange(hstRef),
|
||||
}
|
||||
// 确定按钮
|
||||
function handleConfirm() {
|
||||
const divisionRowData = division.value.rowData
|
||||
const attributes = {
|
||||
...divisionRowData.attributes,
|
||||
baseNumberRange: {
|
||||
division: { selectedIds: selectedIdsMap.value.division },
|
||||
measure: { selectedIds: selectedIdsMap.value.measure },
|
||||
}
|
||||
}
|
||||
|
||||
const divisionCount = selectedIdsMap.value.division?.length || 0
|
||||
const measureCount = selectedIdsMap.value.measure?.length || 0
|
||||
const totalCount = divisionCount + measureCount
|
||||
|
||||
division.value.instance.setDataAtRowProp([
|
||||
[division.value.row, 'baseNumberRange', totalCount > 0 ? `已选${totalCount}` : ''],
|
||||
[division.value.row, 'attributes', attributes]
|
||||
], 'update')
|
||||
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 显示弹窗(暴露给父组件调用)
|
||||
function show(selectedNode: any,params: any) {
|
||||
visible.value = true
|
||||
division.value = params;
|
||||
catalogItemId.value = params.rowData.catalogItemId
|
||||
selectedIdsMap.value = {
|
||||
division: params.rowData.attributes?.baseNumberRange?.division?.selectedIds || [],
|
||||
measure: params.rowData.attributes?.baseNumberRange?.measure?.selectedIds || [],
|
||||
}
|
||||
console.log('show-baseNumberRange',selectedNode, params)
|
||||
setTimeout(() => {
|
||||
loadData(segmentedValue.value)
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ show, close })
|
||||
// 转换分部分项树数据为表格格式
|
||||
function transformDivisionData(items: any[], parentId: string | number | null = null): any[] {
|
||||
return items
|
||||
.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
parentId: parentId,
|
||||
sortOrder: item.nodeType === 'boq' ? (item.sortOrder ?? '') : '',
|
||||
code: item.code || '',
|
||||
nodeType: getDictLabel(DICT_TYPE.DIVISION_CATEGORY, String(item.nodeType)),
|
||||
rawNodeType: item.nodeType,
|
||||
name: item.name || '',
|
||||
selected: selectedIdsMap.value[segmentedValue.value]?.includes(item.id) || false,
|
||||
// originalData: item,
|
||||
// selected: division.value.rowData.attributes?.baseNumberRange?.selectedIds?.includes(item.id) || false,
|
||||
__children: item.children ? transformDivisionData(item.children, item.id) : [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function loadData(tabType: string) {
|
||||
//division.value.rowData.tabType
|
||||
const res = await getDivisionTemplateTree(division.value.rowData.catalogItemId, tabType);
|
||||
// const treeData = await getBoqDivisionTree(compileTreeId);
|
||||
const data = transformDivisionData(res);
|
||||
|
||||
hstRef.value.hotInstance.__onCheckboxChange = handleCheckboxChange
|
||||
hstRef.value.nestedRowsLoadData(data);
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (data: any) =>{
|
||||
console.log('handleCheckboxChange', data)
|
||||
selectedIdsMap.value[segmentedValue.value] = data
|
||||
}
|
||||
|
||||
// 监听类别切换
|
||||
watch(segmentedValue, (val) => {
|
||||
console.log('val',val)
|
||||
loadData(val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="visible"
|
||||
destroy-on-close
|
||||
draggable
|
||||
@update:model-value="(val: boolean) => visible = val"
|
||||
@closed="close"
|
||||
title="基数范围"
|
||||
width="60%"
|
||||
>
|
||||
<div class="flex flex-col h-[500px]">
|
||||
<div class="flex flex-col items-start gap-4">
|
||||
<ElSegmented class="base-number-range-settings-segmented" v-model="segmentedValue" :options="segmentedOptions" size="small" />
|
||||
</div>
|
||||
<DbHst ref="hstRef" :settings="settings" v-show="visible" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.base-number-range-settings-segmented{
|
||||
--el-border-radius-base: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { getRowData, getCellValueByProp } from '#/components/db-hst/command'
|
||||
|
||||
const { EventManager } = Handsontable
|
||||
const { addClass, hasClass } = Handsontable.dom
|
||||
|
||||
//已定位问题根因:自定义 renderer 在隐藏列后因 DOM 复用产生脏渲染,不是单纯没刷新整表。
|
||||
export const rateRenderer = (instance: any, TD: HTMLTableCellElement, row: number, col: number, prop: string | number, value: any, cellProperties: any) => {
|
||||
// 先重绘 TD,防止 DOM 复用时残留旧内容
|
||||
Handsontable.renderers.TextRenderer.apply(this, [instance, TD, row, col, prop, value, cellProperties])
|
||||
|
||||
const rowData = getRowData(instance, row)
|
||||
const isBoqNode = rowData?.nodeType === 'boq'
|
||||
if (!isBoqNode) return;
|
||||
|
||||
const { rootDocument } = instance;
|
||||
const ICON = rootDocument.createElement('DIV');
|
||||
|
||||
ICON.className = 'htMenuLine';
|
||||
ICON.appendChild(rootDocument.createTextNode(String.fromCharCode(9660)));
|
||||
|
||||
if (!TD.firstChild) { TD.appendChild(rootDocument.createTextNode(String.fromCharCode(160))); }
|
||||
|
||||
const targetIcon = 'htRateIcon'
|
||||
TD.insertBefore(ICON, TD.firstChild);
|
||||
addClass(ICON, ['!cursor-pointer', targetIcon]);
|
||||
|
||||
if (!instance[targetIcon + 'Listener']) {
|
||||
const eventManager = new EventManager(instance);
|
||||
instance[targetIcon + 'Listener'] = function (event: MouseEvent) {
|
||||
if (hasClass(event.target as HTMLElement, targetIcon)) {
|
||||
const targetCell = (event.target as HTMLElement).closest('td');
|
||||
|
||||
if (targetCell) {
|
||||
const coords = instance.getCoords(targetCell);
|
||||
const cellMeta = instance.getCellMeta(coords.row, coords.col)
|
||||
if (cellMeta.readOnly != undefined && cellMeta.readOnly) return;
|
||||
if (cellMeta.editor != undefined && !cellMeta.editor) return;
|
||||
const { cellProp, cellValue, rowData } = getCellValueByProp(instance, coords.row, coords.col)
|
||||
//执行自定义方法
|
||||
if (instance.__onRate) {
|
||||
instance.__onRate({
|
||||
instance,
|
||||
TD,
|
||||
row: coords.row,
|
||||
column: coords.col,
|
||||
prop: cellProp,
|
||||
value: cellValue,
|
||||
cellProperties: cellMeta,
|
||||
rowData,
|
||||
})
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
eventManager.addEventListener(instance.rootElement, 'mousedown', instance[targetIcon + 'Listener'], true);
|
||||
|
||||
instance.addHookOnce('afterDestroy', () => {
|
||||
eventManager.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts" setup>
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog, ElSplitter, ElSplitterPanel, ElCard, ElSelect } from 'element-plus';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { getQuotaCatalogItemChildren } from '#/api/database/quota';
|
||||
import { getDirectoryTree, getRateItemList } from '#/api/database/interface/cardinalRate';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'success', data: any): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// 内部状态
|
||||
const visible = ref(false)
|
||||
// 专业相关定义
|
||||
const majors = ref<string | null>(null)
|
||||
const majorsOptions = ref<any[]>([])
|
||||
const treeData = ref<any[]>([])
|
||||
const hstRef = ref<any>()
|
||||
const selectedNode = ref<any>()
|
||||
const division = ref<any>()
|
||||
const selectedRow = ref<any>()
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{ type: 'text', data: 'sortOrder', title: '序号', width: 60, editor: false },
|
||||
{ type: 'text', data: 'name', title: '名称', editor: false },
|
||||
{ type: 'text', data: 'rate', title: '费率', editor: false },
|
||||
// { type: 'text', data: 'calcBase', title: '计算基数' },
|
||||
{ type: 'text', data: 'remark', title: '备注', editor: false },
|
||||
]
|
||||
|
||||
const settings = {
|
||||
data: [],
|
||||
colWidths: 150,
|
||||
columns: columns,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
beforeOnCellMouseDown(this: any, event: MouseEvent, coords: any, _TD: HTMLTableCellElement) {
|
||||
if (event.detail === 1 && event.button === 0) {
|
||||
const rowData = this.getSourceDataAtRow(coords.row)
|
||||
if(rowData) selectedRow.value = rowData
|
||||
}
|
||||
},
|
||||
}
|
||||
// 确定按钮
|
||||
function handleConfirm() {
|
||||
// emit('success', { formula: formula.value })
|
||||
// console.log('handleConfirm', selectedRow.value)
|
||||
division.value.instance.setDataAtRowProp([
|
||||
[division.value.row, 'rate', selectedRow.value.rate],
|
||||
[division.value.row, 'remark', selectedRow.value.remark]
|
||||
], 'update')
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 显示弹窗(暴露给父组件调用)
|
||||
function show(_selectedNode: any,params: any) {
|
||||
visible.value = true
|
||||
selectedNode.value = _selectedNode
|
||||
division.value = params;
|
||||
console.log('show-rate', selectedNode.value, params)
|
||||
setTimeout(() => {
|
||||
loadData()
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ show, close })
|
||||
|
||||
// 加载变量设置(一次性获取所有定额专业的数据)
|
||||
async function loadData() {
|
||||
majors.value = null;
|
||||
treeData.value = []
|
||||
const options = await getQuotaCatalogItemChildren(selectedNode.value.parentId)
|
||||
majorsOptions.value = options.filter(f=>f.nodeType === 'majors').map(item=>({
|
||||
label: item.name,
|
||||
value: String(item.id)
|
||||
}))
|
||||
if(majorsOptions.value.length > 0) {
|
||||
majors.value = majorsOptions.value[0]?.value
|
||||
handleSelectChange(majors.value)
|
||||
}
|
||||
hstRef.value.hotInstance.loadData([])
|
||||
}
|
||||
async function handleSelectChange(value: any) {
|
||||
hstRef.value.hotInstance.loadData([])
|
||||
const tree = await getDirectoryTree(value)
|
||||
console.log('tree',tree)
|
||||
treeData.value = convertCatalogTree(tree)
|
||||
}
|
||||
|
||||
function convertCatalogTree(items: any[]): any[] {
|
||||
return items.map(item => {
|
||||
const node: any = {
|
||||
id: String(item.id),
|
||||
label: item.name,
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
node.children = convertCatalogTree(item.children)
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
// 处理节点点击
|
||||
async function handleNodeClick(option: any) {
|
||||
console.log('node click:', option)
|
||||
// here you can add processing logic
|
||||
const res = await getRateItemList(option.data.id)
|
||||
hstRef.value.hotInstance.loadData(res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 基数费率弹窗 -->
|
||||
<ElDialog :model-value="visible" destroy-on-close draggable @update:model-value="(val: boolean) => visible = val"
|
||||
@closed="close" title="基数费率" width="60%">
|
||||
<div class="flex flex-col h-[500px]">
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="30%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<ElSelect v-model="majors" :options="majorsOptions" size="small" @change="handleSelectChange"/>
|
||||
<DbTree ref="dbTreeRef" :tree-data="treeData" :edit-on-dblclick="false" @node-click="handleNodeClick" :defaultExpandedLevel="2"/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings"></DbHst>
|
||||
</div>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import ColoumApply from '#/components/db-hst/component/ColoumApply.vue';
|
||||
import BaseNumber from '../baseNumber/index.vue';
|
||||
import BaseNumberRange from '../baseNumberRange/index.vue';
|
||||
import Rate from '../rate/index.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
|
||||
import { useHst } from './use-hst';
|
||||
import { getWorkbenchHiddenFields, getDivisionTemplateTree } from '#/api/database/interface/config';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
tabType: string;
|
||||
}>();
|
||||
|
||||
const hstRef = ref();
|
||||
const coloumApplyRef = ref();
|
||||
const baseNumberRef = ref();
|
||||
const baseNumberRangeRef = ref();
|
||||
const rateRef = ref();
|
||||
|
||||
const unitFields = ref()
|
||||
type TabType = 'division' | 'measure' | 'other' | 'unit_summary'
|
||||
|
||||
const tabHiddenFieldsKeyMap: Record<TabType, string> = {
|
||||
division: 'divisionHiddenFields',
|
||||
measure: 'measureHiddenFields',
|
||||
other: 'otherHiddenFields',
|
||||
unit_summary: 'summaryHiddenFields',
|
||||
}
|
||||
|
||||
const hiddenField = computed(()=> unitFields.value[tabHiddenFieldsKeyMap[props.tabType as TabType]])
|
||||
const { columns, hstInit, hstSettings, hstContextMenuItems } = useHst(hstRef);
|
||||
|
||||
/** recursive convert children to __children for nestedRows */
|
||||
const convertToNestedData = (items: any[]): any[] => {
|
||||
return items.map((item: any) => {
|
||||
const divisionSelectedIds = item.attributes?.baseNumberRange?.division?.selectedIds || []
|
||||
const measureSelectedIds = item.attributes?.baseNumberRange?.measure?.selectedIds || []
|
||||
const totalCount = divisionSelectedIds.length + measureSelectedIds.length
|
||||
const converted: any = {
|
||||
...item,
|
||||
category: item.nodeType ? getDictLabel(DICT_TYPE.DIVISION_CATEGORY, item.nodeType) : item.category,
|
||||
baseNumber: item.attributes?.baseNumber?.formula || '',
|
||||
baseNumberRange: totalCount > 0 ? `已选${totalCount}` : ''
|
||||
};
|
||||
converted.__children = (item.children && item.children.length > 0) ? convertToNestedData(item.children) : [];
|
||||
|
||||
return converted;
|
||||
});
|
||||
};
|
||||
|
||||
const loadAndFillData = async (catalogItemId: number) => {
|
||||
//根据接口来显示隐藏settings.columns
|
||||
unitFields.value = await getWorkbenchHiddenFields(catalogItemId);
|
||||
|
||||
// console.log('res',unitFields.value, props.tabType, hiddenField.value)
|
||||
|
||||
const res = await getDivisionTemplateTree(catalogItemId, props.tabType);
|
||||
// // console.log('res', res)
|
||||
|
||||
const data = convertToNestedData(res)
|
||||
console.log('data',data)
|
||||
hstRef.value.nestedRowsLoadData(data)
|
||||
|
||||
hstRef.value.hotInstance.__onColumnApply = handleColumnApply//隐藏列头
|
||||
hstRef.value.hotInstance.__onBaseNumber = handleBaseNumber
|
||||
hstRef.value.hotInstance.__onBaseNumberRange = handleBaseNumberRange
|
||||
hstRef.value.hotInstance.__onRate = handleRate
|
||||
|
||||
hstRef.value.tabType = props.tabType;
|
||||
hstRef.value.catalogItemId = catalogItemId;
|
||||
|
||||
|
||||
handleColumns(hiddenField.value)
|
||||
};
|
||||
onMounted(() => {
|
||||
console.log('onMounted-division')
|
||||
setTimeout(() => {
|
||||
if (props.selectedNode) {
|
||||
loadAndFillData(props.selectedNode.id);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedNode,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadAndFillData(val.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
const handleColumns = (hiddenFieldCodes: string[]) => {
|
||||
const hotInstance = hstRef.value?.hotInstance
|
||||
if (!hotInstance) return
|
||||
// Get column indexes to hide based on original columns configuration
|
||||
const hiddenColumnIndexes = columns
|
||||
.map((col: any, index: number) => ({ col, index }))
|
||||
.filter(({ col }) => hiddenFieldCodes.includes(col.data))
|
||||
.map(({ index }) => index);
|
||||
|
||||
const hiddenColumnsPlugin = hotInstance.getPlugin('hiddenColumns');
|
||||
if (hiddenColumnsPlugin) {
|
||||
hotInstance.batchExecution(() => {
|
||||
// First show all columns to reset state
|
||||
hiddenColumnsPlugin.showColumns(hiddenColumnsPlugin.getHiddenColumns());
|
||||
// Then hide the specified columns
|
||||
if (hiddenColumnIndexes.length > 0) {
|
||||
hiddenColumnsPlugin.hideColumns(hiddenColumnIndexes);
|
||||
}
|
||||
}, true)
|
||||
hotInstance.refreshDimensions()
|
||||
hotInstance.render()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSuccessColumnApply = (data: any[]) => {
|
||||
console.log('handleColumnApply', data);
|
||||
// unitFields.value = data;
|
||||
coloumApplyRef.value?.close();
|
||||
handleColumns(data);
|
||||
};
|
||||
|
||||
const handleColumnApply = () => {
|
||||
coloumApplyRef.value?.show(hiddenField.value, props.selectedNode.id);
|
||||
};
|
||||
const handleBaseNumber = (params: any) => {
|
||||
console.log('handleBaseNumber', params);
|
||||
baseNumberRef.value?.show(props.selectedNode, params);
|
||||
};
|
||||
const handleBaseNumberSuccess = (data: any) => {
|
||||
console.log('handleBaseNumberSuccess', data);
|
||||
// TODO: 处理基数设置成功后的逻辑
|
||||
};
|
||||
|
||||
const handleBaseNumberRange = (params: any) => {
|
||||
console.log('handleBaseNumberRange', params);
|
||||
baseNumberRangeRef.value?.show(props.selectedNode, params);
|
||||
};
|
||||
const handleBaseNumberRangeSuccess = (data: any) => {
|
||||
console.log('handleBaseNumberRangeSuccess', data);
|
||||
|
||||
};
|
||||
|
||||
const handleRate = (params: any) => {
|
||||
console.log('handleRate', params);
|
||||
rateRef.value?.show(props.selectedNode,params);
|
||||
};
|
||||
const handleRateSuccess = (data: any) => {
|
||||
console.log('handleRateSuccess', data);
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="hstSettings" :contextMenuItems="hstContextMenuItems" />
|
||||
<ColoumApply ref="coloumApplyRef" :success="handleSuccessColumnApply" />
|
||||
<BaseNumber ref="baseNumberRef" @success="handleBaseNumberSuccess" />
|
||||
<BaseNumberRange ref="baseNumberRangeRef" @success="handleBaseNumberRangeSuccess" />
|
||||
<Rate ref="rateRef" @success="handleRateSuccess" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!-- <script setup lang="ts">
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { ElButton, ElDialog } from 'element-plus';
|
||||
import { ref } from 'vue'
|
||||
import { saveUnitTabRefs, getUnitDivisionTemplateTree } from '#/api/database/interface/config';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
success?: (data: any[]) => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const modelValue = ref(false)
|
||||
const hstRef = ref()
|
||||
|
||||
const columns = [
|
||||
{ type: 'db.nestedRows', data: 'sortOrder', title: '序号', editor: false },
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'category', title: '类别', className: 'htCenter', editor: false },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'checkbox', data: 'selected', title: '选择', className: 'htCenter' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 递归设置所有子节点的选中状态
|
||||
*/
|
||||
const setChildrenSelected = (node: any, selected: boolean): void => {
|
||||
if (!node.__children?.length) return;
|
||||
|
||||
node.__children.forEach((child: any) => {
|
||||
child.selected = selected;
|
||||
setChildrenSelected(child, selected);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归检查并更新父节点的选中状态
|
||||
* 只要有子节点选中,父节点就选中
|
||||
*/
|
||||
const updateParentSelected = (node: any, dataManager: any): void => {
|
||||
const parent = dataManager.getRowParent(node);
|
||||
if (!parent) return;
|
||||
|
||||
// 检查是否有子节点被选中
|
||||
parent.selected = parent.__children?.some((child: any) => child.selected) ?? false;
|
||||
|
||||
// 递归向上更新父节点
|
||||
updateParentSelected(parent, dataManager);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理 checkbox 变化事件
|
||||
*/
|
||||
const handleCheckboxChange = (changes: any[] | null, source: string): void => {
|
||||
if (!changes || source === 'loadData') return;
|
||||
|
||||
const hotInstance = hstRef.value?.hotInstance;
|
||||
if (!hotInstance) return;
|
||||
|
||||
const nestedRowsPlugin = hotInstance.getPlugin('nestedRows');
|
||||
if (!nestedRowsPlugin?.enabled) return;
|
||||
|
||||
const dataManager = nestedRowsPlugin.dataManager;
|
||||
|
||||
for (const [row, prop, , newValue] of changes) {
|
||||
if (prop !== 'selected') continue;
|
||||
|
||||
const dataObject = dataManager.getDataObject(row);
|
||||
if (!dataObject) continue;
|
||||
|
||||
// 如果是父节点,递归设置所有子节点的选中状态
|
||||
if (dataManager.isParent(row)) {
|
||||
setChildrenSelected(dataObject, newValue);
|
||||
}
|
||||
|
||||
// 向上更新父节点的选中状态
|
||||
updateParentSelected(dataObject, dataManager);
|
||||
}
|
||||
|
||||
hotInstance.render();
|
||||
};
|
||||
|
||||
const settings = {
|
||||
data: [],
|
||||
columns,
|
||||
afterChange: handleCheckboxChange,
|
||||
}
|
||||
const convertToNestedData = (items: any[]): any[] => {
|
||||
return items.map((item: any) => {
|
||||
const converted: any = {
|
||||
...item,
|
||||
category: item.nodeType ? getDictLabel(DICT_TYPE.DIVISION_CATEGORY, item.nodeType) : item.category,
|
||||
};
|
||||
converted.__children = (item.children && item.children.length > 0) ? convertToNestedData(item.children) : [];
|
||||
|
||||
return converted;
|
||||
});
|
||||
};
|
||||
const catalogItemId = ref()
|
||||
const tabType = ref()
|
||||
const show = async (selectedNode: any, type: string) => {
|
||||
modelValue.value = true;
|
||||
catalogItemId.value = selectedNode.id
|
||||
tabType.value = type
|
||||
// console.log('selectedNode',selectedNode)
|
||||
const res = await getUnitDivisionTemplateTree(selectedNode.id);
|
||||
setTimeout(() => {
|
||||
hstRef.value?.nestedRowsLoadData(convertToNestedData(res));
|
||||
}, 200);
|
||||
}
|
||||
const close = () => {
|
||||
modelValue.value = false
|
||||
}
|
||||
const handleConfirm = () => {
|
||||
const hotInstance = hstRef.value?.hotInstance
|
||||
const data = hotInstance?.getSourceData() ?? []
|
||||
const res = data.filter((f:any)=>f.selected).map((item:any)=>item.id)
|
||||
console.log('handleConfirm',res)
|
||||
// props.success?.(result)
|
||||
// modelValue.value = false
|
||||
saveUnitTabRefs({catalogItemId: catalogItemId.value, tabType: tabType.value, templateNodeIds: res})
|
||||
}
|
||||
|
||||
defineExpose({ show, close })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog :model-value="modelValue" @update:model-value="close" title="引用-分部分项"
|
||||
width="700px" :close-on-click-modal="false">
|
||||
<div class="h-[400px] w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="settings" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style> -->
|
||||
@@ -0,0 +1,319 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { workbenchColumns } from '../../configs';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { createDivisionTemplate, deleteDivisionTemplate, updateDivisionTemplate } from '#/api/database/interface/config';
|
||||
|
||||
let contextMenuOnHeader = false
|
||||
|
||||
const columns: any[] = workbenchColumns.map((item: any) => ({
|
||||
...item,
|
||||
width: 120,
|
||||
|
||||
}));
|
||||
|
||||
const handleContextMenu = (hstRef: Ref<any>) => {
|
||||
return {
|
||||
items: {
|
||||
// row_above: {},
|
||||
// row_below: {},
|
||||
// remove_row: {},
|
||||
insert_division: {
|
||||
name: '插入分部',
|
||||
callback(this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// const row = this.getSelectedLast?.()[0]
|
||||
// const hotInstance = hstRef.value.hotInstance
|
||||
const row = this.countRows()
|
||||
console.log('row', row)
|
||||
this.alter('insert_row_below', row, 1);
|
||||
this.setDataAtRowProp([
|
||||
[row, 'nodeType', 'division'],
|
||||
[row, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, 'division')],
|
||||
[row, 'name', '新建分部'],
|
||||
[row, 'tabType', hstRef.value.tabType],
|
||||
[row, 'code', String(new Date().getTime())],
|
||||
], 'insertRow')
|
||||
}
|
||||
},
|
||||
insert_child_division: {
|
||||
name: '插入子分部',
|
||||
disabled(this: any): boolean {
|
||||
// 只在分部行上可用
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
return rowData?.nodeType !== 'division'
|
||||
},
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const hotInstance = this
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
// // 子分部的父节点就是当前选中的分部
|
||||
const parentId = rowData?.id
|
||||
|
||||
// // 先调用API创建节点
|
||||
const data = {
|
||||
catalogItemId: hstRef.value.catalogItemId,
|
||||
parentId: parentId,
|
||||
nodeType: 'division',
|
||||
name: '新建子分部',
|
||||
tabType: hstRef.value.tabType,
|
||||
code: String(new Date().getTime())
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await createDivisionTemplate(data)
|
||||
console.log('插入子分部: API创建成功,ID:', data)
|
||||
if (!id) return;
|
||||
|
||||
// // 创建成功后,作为选中分部的子节点插入
|
||||
hstRef?.value.addChild().then((res: any) => {
|
||||
// // 创建成功后,在表格中插入新行
|
||||
hotInstance.setDataAtRowProp([
|
||||
[res, 'id', id],
|
||||
[res, 'nodeType', data.nodeType],
|
||||
[res, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
[res, 'name', data.name],
|
||||
[res, 'parentId', data.parentId],
|
||||
[res, 'code', data.code],
|
||||
], 'created')
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('插入子分部: API创建失败', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
insert_boq: {
|
||||
name: '插入清单',
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const hotInstance = hstRef.value.hotInstance
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// console.log('插入清单: 选中行', row, '节点类型', rowData?.nodeType, '节点ID', rowData?.id, '父节点ID', rowData?.parentId)
|
||||
|
||||
// 确定父节点ID
|
||||
let parentId: any = null
|
||||
const nodeType = rowData?.nodeType
|
||||
if (nodeType === 'division') {
|
||||
parentId = rowData?.id
|
||||
} else if (nodeType === 'boq') {
|
||||
parentId = rowData?.parentId
|
||||
} else if (nodeType === 'quota') {
|
||||
// const parentBoqData = findParentByType(this, row, 'boq')
|
||||
// parentId = parentBoqData?.parentId
|
||||
// operationType = 'below'
|
||||
//this.alter('insert_row_below', row, 1)
|
||||
} else {
|
||||
console.warn('清单只能在根目录或分部节点下创建,当前节点类型:', rowData?.nodeType)
|
||||
return
|
||||
}
|
||||
|
||||
// 先调用API创建节点
|
||||
const data = {
|
||||
catalogItemId: hstRef.value.catalogItemId,
|
||||
parentId: parentId,
|
||||
nodeType: 'boq',
|
||||
name: '新建清单',
|
||||
tabType: hstRef.value.tabType,
|
||||
code: String(new Date().getTime())
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await createDivisionTemplate(data)
|
||||
console.log('插入清单: API创建成功,ID:', data)
|
||||
if (!id) return;
|
||||
hstRef?.value.addChild().then((res: any) => {
|
||||
// 获取父节点对象,统计已有 boq 子节点数量
|
||||
const plugin = this.getPlugin('nestedRows');
|
||||
const parentObj = plugin.dataManager.getDataObject(row);
|
||||
const children = parentObj?.__children || [];
|
||||
const boqCount = children.filter((child: any) => child.nodeType === 'boq').length;
|
||||
const sortOrder = boqCount + 1;
|
||||
|
||||
// 创建成功后,在表格中插入新行
|
||||
hotInstance.setDataAtRowProp([
|
||||
[res, 'sortOrder', sortOrder],
|
||||
[res, 'id', id],
|
||||
[res, 'nodeType', data.nodeType],
|
||||
[res, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
[res, 'name', data.name],
|
||||
[res, 'parentId', parentId],
|
||||
[res, 'code', data.code],
|
||||
], 'created')
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('插入清单: API创建失败', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
separator1: '---------',
|
||||
remove_row: {
|
||||
name: '删除',
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
if (rowData.id) {
|
||||
deleteDivisionTemplate(rowData.id)
|
||||
}
|
||||
this.alter('remove_row', row, 1);
|
||||
}
|
||||
},
|
||||
col_show: {
|
||||
name: '列头筛选',
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
console.log("列头筛选")
|
||||
if (hstRef.value.hotInstance.__onColumnApply) hstRef.value.hotInstance.__onColumnApply()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
export const useHst = (hstRef: Ref<any>) => {
|
||||
const hstSettings = {
|
||||
data: [],
|
||||
colWidths: 100,
|
||||
columns,
|
||||
colHeaders: true,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
hiddenColumns: {
|
||||
copyPasteEnabled: true,
|
||||
indicators: false,
|
||||
},
|
||||
contextMenu: handleContextMenu(hstRef),
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
afterChange(this: any, changes: any[], source: string) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId' || source === 'created') return;
|
||||
const hasRealChange = changes.some((change: any) => {
|
||||
const [, , oldValue, newValue] = change;
|
||||
const isEmpty = (v: any) => v === null || v === undefined || v === '';
|
||||
return oldValue !== newValue && !(isEmpty(oldValue) && isEmpty(newValue));
|
||||
})
|
||||
if (!hasRealChange) return;
|
||||
const row = changes[0][0];
|
||||
const rowData = this.getSourceDataAtRow(row);
|
||||
const data = {
|
||||
...rowData,
|
||||
catalogItemId: hstRef?.value?.catalogItemId,
|
||||
tabType: rowData?.tabType == undefined ? hstRef.value.tabType : rowData?.tabType,
|
||||
code: rowData?.code == undefined ? String(new Date().getTime()) : rowData?.code,
|
||||
}
|
||||
console.log('data', data);
|
||||
|
||||
if (rowData.id == null) {
|
||||
createDivisionTemplate(data).then(res => {
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateDivisionTemplate(data).then(res => {
|
||||
console.log('updateUnitDivisionTemplate', res)
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeOnCellContextMenu(event: MouseEvent, coords: any) {
|
||||
// coords.row < 0 表示右键点击的是列头
|
||||
contextMenuOnHeader = coords.row < 0
|
||||
},
|
||||
beforeContextMenuSetItems(menuItems: any[]) {
|
||||
const headerColumns = ['col_show']
|
||||
// 必须原地修改数组,因为 Handsontable runHooks 不使用钩子返回值
|
||||
const filtered = contextMenuOnHeader
|
||||
? menuItems.filter(item => headerColumns.includes(item.key))
|
||||
: menuItems.filter(item => !headerColumns.includes(item.key))
|
||||
menuItems.splice(0, menuItems.length, ...filtered)
|
||||
},
|
||||
afterRenderer(this: any, TD: HTMLTableCellElement, row: number, col: number, prop: string, value: any, cellProperties: any) {
|
||||
if (col == 0) return;
|
||||
// afterRenderer 的 row 是 visual index,getSourceDataAtRow 需要 physical index
|
||||
const physicalRow = this.toPhysicalRow(row);
|
||||
const rowData = this.getSourceDataAtRow(physicalRow);
|
||||
let nodeType = rowData?.nodeType;
|
||||
|
||||
// 移除旧的 node-type-* class,防止 TD 复用时 class 累积
|
||||
const nodeTypeClasses = TD.className.match(/node-type-\S+/g);
|
||||
if (nodeTypeClasses) {
|
||||
nodeTypeClasses.forEach((cls: string) => TD.classList.remove(cls));
|
||||
}
|
||||
|
||||
if (nodeType) {
|
||||
const nodeTypeClass = `node-type-${nodeType}`;
|
||||
TD.classList.add(nodeTypeClass);
|
||||
// 单位工程(root)类型:只允许编辑费用代号字段
|
||||
if (nodeType === 'root') {
|
||||
// 费用代号字段可编辑,其他字段只读
|
||||
if (prop !== 'costCode') {
|
||||
cellProperties.editor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const hstContextMenuItems = [
|
||||
{
|
||||
key: 'insert',
|
||||
name: '插入行',
|
||||
callback: async (hotInstance: any) => {
|
||||
if (!hstRef.value.catalogItemId) {
|
||||
ElMessage.error('请选择左侧专业类别')
|
||||
return
|
||||
}
|
||||
// // 先调用API创建节点
|
||||
let data: any = {
|
||||
catalogItemId: hstRef.value.catalogItemId,
|
||||
nodeType: 'division',
|
||||
name: '新建分部',
|
||||
tabType: hstRef.value.tabType,
|
||||
code: String(new Date().getTime())
|
||||
}
|
||||
try {
|
||||
|
||||
const id = await createDivisionTemplate(data)
|
||||
console.log('插入根分部: API创建成功,ID:', data)
|
||||
if(!id) return;
|
||||
const sourceData = hstRef.value.hotInstance.getSourceData()
|
||||
if(sourceData.length == 0){
|
||||
data.id = id
|
||||
hstRef.value.nestedRowsLoadData([data])
|
||||
}else{
|
||||
const row = hotInstance.countRows()
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1)
|
||||
hotInstance.setDataAtRowProp([
|
||||
[row, 'id', id],
|
||||
[row, 'nodeType', data.nodeType],
|
||||
[row, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
[row, 'name', data.name],
|
||||
[row, 'tabType', data.tabType],
|
||||
[row, 'code', data.code],
|
||||
], 'created')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('插入子分部: API创建失败', err)
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const hstInit = () => {
|
||||
hstRef.value.nestedRowsLoadData([]);
|
||||
};
|
||||
|
||||
return {
|
||||
columns,
|
||||
hstInit,
|
||||
hstSettings,
|
||||
hstContextMenuItems,
|
||||
};
|
||||
};
|
||||
105
apps/web-ele/src/views/database/interface/unitConfig/configs.ts
Normal file
105
apps/web-ele/src/views/database/interface/unitConfig/configs.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions, getDictObj } from '@vben/hooks';
|
||||
import { baseNumberRenderer } from './components/baseNumber/index';
|
||||
import { baseNumberRangeRenderer } from './components/baseNumberRange/index';
|
||||
import { rateRenderer } from './components/rate/index';
|
||||
// 获取字典颜色(css_class)
|
||||
export function getDictColor(rowData: any): string {
|
||||
let nodeType = rowData.nodeType
|
||||
if (nodeType === null || nodeType === undefined) return ''
|
||||
if (nodeType == 'boq' && rowData.isSyncSource) {
|
||||
nodeType = 'sync_source'
|
||||
}
|
||||
const dictData = getDictObj(DICT_TYPE.DIVISION_CATEGORY, nodeType)
|
||||
return dictData?.cssClass || '#FFFFFF'
|
||||
}
|
||||
//分部分项 措施项目 其他项目
|
||||
export const workbenchColumns = [
|
||||
{ type: 'db.nestedRows', data: 'sortOrder', title: '序号', colorClass: (rowData: any) => getDictColor(rowData), },
|
||||
{ type: 'text', data: 'sortOrderCustom', title: '序号-自定义', remark: '自定义序号' },//自定义
|
||||
{ type: 'text', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'category', title: '类别', className: 'htCenter', editor: false },
|
||||
{ type: 'checkbox', data: 'tempDelete', title: '临删', className: 'htCenter' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'feature', title: '项目特征' },
|
||||
{ type: 'text', data: 'unit', title: '单位', editor: false, renderer: 'db-dropdown', source: getDictOptions(DICT_TYPE.MATERIAL_UNIT) },
|
||||
{ type: 'numeric', data: 'quantity', title: '工程量' },
|
||||
{ type: 'numeric', data: 'unitPrice', title: '单价' },
|
||||
{ type: 'numeric', data: 'fixedPrice', title: '固定单价' },
|
||||
{ type: 'text', data: 'baseNumber', title: '基数', renderer: baseNumberRenderer},
|
||||
{ type: 'text', data: 'baseNumberRange', title: '基数范围', renderer: baseNumberRangeRenderer },
|
||||
{ type: 'numeric', data: 'rate', title: '费率', renderer: rateRenderer },
|
||||
{ type: 'numeric', data: 'totalPrice', title: '合价' },
|
||||
{ type: 'numeric', data: 'indicator', title: '指标' },
|
||||
{ type: 'numeric', data: 'compareQuantity', title: '对比量' },
|
||||
{ type: 'numeric', data: 'comparePrice', title: '对比价' },
|
||||
{ type: 'numeric', data: 'compareTotalPrice', title: '对比合价' },
|
||||
{ type: 'text', data: 'costProfession', title: '取费专业' },
|
||||
{ type: 'text', data: 'costCode', title: '费用代号' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
|
||||
{ type: 'numeric', data: 'laborUnitPrice', title: '人工费单价' },
|
||||
{ type: 'numeric', data: 'materialUnitPrice', title: '材料费单价' },
|
||||
{ type: 'numeric', data: 'machineUnitPrice', title: '机械费单价' },
|
||||
{ type: 'numeric', data: 'managementUnitPrice', title: '管理费单价' },
|
||||
{ type: 'numeric', data: 'profitUnitPrice', title: '利润单价' },
|
||||
{ type: 'numeric', data: 'regulatoryUnitPrice', title: '规费单价' },
|
||||
{ type: 'numeric', data: 'taxUnitPrice', title: '税金单价' },
|
||||
{ type: 'numeric', data: 'laborCost', title: '人工费' },
|
||||
{ type: 'numeric', data: 'materialCost', title: '材料费' },
|
||||
{ type: 'numeric', data: 'machineCost', title: '机械费' },
|
||||
{ type: 'numeric', data: 'measuresCost', title: '措施费' },
|
||||
{ type: 'numeric', data: 'managementCost', title: '管理费' },
|
||||
{ type: 'numeric', data: 'profit', title: '利润' },
|
||||
{ type: 'numeric', data: 'regulatoryCost', title: '规费' },
|
||||
{ type: 'numeric', data: 'tax', title: '税金' },
|
||||
];
|
||||
|
||||
//子目工料机,清单工料机,市场主材设备
|
||||
export const materialSummaryColumns = [
|
||||
{ type: 'db.nestedRows', data: 'code', title: '编码' },
|
||||
{ type: 'text', data: 'name', title: '名称' },
|
||||
{ type: 'text', data: 'spec', title: '型号规格' },
|
||||
{ type: 'text', data: 'unit', title: '单位' },
|
||||
{ type: 'text', data: 'category', title: '类别' },
|
||||
{ type: 'numeric', data: 'taxRate', title: '税率' },
|
||||
{ type: 'numeric', data: 'basePriceExTax', title: '除税基价' },
|
||||
{ type: 'numeric', data: 'basePriceInTax', title: '含税基价' },
|
||||
{ type: 'numeric', data: 'compilePriceExTax', title: '除税编制价' },
|
||||
{ type: 'numeric', data: 'compilePriceInTax', title: '含税编制价' },
|
||||
{ type: 'numeric', data: 'quotaQuantity', title: '定额消耗量' },
|
||||
{ type: 'numeric', data: 'adjustQuantity', title: '调整消耗量' },
|
||||
{ type: 'numeric', data: 'usageQty', title: '用量' },
|
||||
{ type: 'numeric', data: 'usageTotalExTax', title: '除税合价' },
|
||||
{ type: 'numeric', data: 'usageTotalInTax', title: '含税合价' },
|
||||
{ type: 'numeric', data: 'totalBasePriceExTax', title: '除税基价合价' },
|
||||
{ type: 'numeric', data: 'totalBasePriceInTax', title: '含税基价合价' },
|
||||
{ type: 'numeric', data: 'totalCompilePriceExTax', title: '除税编制价合价' },
|
||||
{ type: 'numeric', data: 'totalCompilePriceInTax', title: '含税编制价合价' },
|
||||
{ type: 'text', data: 'calcBase', title: '计算基数' },
|
||||
{ type: 'text', data: 'adjustmentFormula', title: '调整公式' },
|
||||
{ type: 'text', data: 'priceSourceText', title: '价格来源' },
|
||||
]
|
||||
//单位汇总
|
||||
export const unitSummary = [
|
||||
{ type: 'text', data: 'divisionalCost', title: '分部分项工程费', _id: 1 },
|
||||
{ type: 'text', data: 'muckTransportCost', title: '余泥渣土运输与排放费用', _parentId: 1 },
|
||||
{ type: 'text', data: 'measureProjectCost', title: '措施项目费', _id: 2 },
|
||||
{ type: 'text', data: 'greenConstructionSafetyCost', title: '绿色施工安全防护措施费', _parentId: 2 },
|
||||
{ type: 'text', data: 'otherMeasureCost', title: '其他措施费', _parentId: 2 },
|
||||
{ type: 'text', data: 'otherProjectCost', title: '其他项目费', _id: 3 },
|
||||
{ type: 'text', data: 'provisionalAmount', title: '暂列金额', _parentId: 3 },
|
||||
{ type: 'text', data: 'provisionalEstimate', title: '暂估价', _parentId: 3 },
|
||||
{ type: 'text', data: 'dayWork', title: '计日工', _parentId: 3 },
|
||||
{ type: 'text', data: 'generalContractServiceCost', title: '总承包服务费', _parentId: 3 },
|
||||
{ type: 'text', data: 'budgetLumpSumCost', title: '预算包干费', _parentId: 3 },
|
||||
{ type: 'text', data: 'highQualityProjectCost', title: '工程优质费', _parentId: 3 },
|
||||
{ type: 'text', data: 'estimateRangeDifference', title: '概算幅度差', _parentId: 3 },
|
||||
{ type: 'text', data: 'claimCost', title: '索赔费用', _parentId: 3 },
|
||||
{ type: 'text', data: 'onSiteVisaCost', title: '现场签证费用', _parentId: 3 },
|
||||
{ type: 'text', data: 'otherCost', title: '其他费用', _parentId: 3 },
|
||||
{ type: 'text', data: 'preTaxProjectCost', title: '税前工程造价', _id: 4 },
|
||||
{ type: 'text', data: 'vatOutputTax', title: '增值税销项税额', _id: 5 },
|
||||
{ type: 'text', data: 'taxIncludedTotalCost', title: '含税工程总造价', _id: 6 },
|
||||
{ type: 'text', data: 'laborCost', title: '人工费', _id: 7 }
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Reference from '../components/reference/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Reference ref="referenceRef" :selectedNode="props.selectedNode" tabType="division"/>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,295 @@
|
||||
// import type { Ref } from 'vue';
|
||||
// import { workbenchColumns } from '../configs';
|
||||
// import { ElMessage } from 'element-plus';
|
||||
// import { getDictLabel } from '@vben/hooks';
|
||||
// import { DICT_TYPE } from '@vben/constants';
|
||||
// import { createDivisionTemplate, deleteDivisionTemplate, updateDivisionTemplate } from '#/api/database/interface/config';
|
||||
|
||||
// let contextMenuOnHeader = false
|
||||
|
||||
// const columns: any[] = workbenchColumns.map((item: any) => ({
|
||||
// ...item,
|
||||
// width: 120,
|
||||
|
||||
// }));
|
||||
|
||||
// const handleContextMenu = (hstRef: Ref<any>) => {
|
||||
// return {
|
||||
// items: {
|
||||
// // row_above: {},
|
||||
// // row_below: {},
|
||||
// // remove_row: {},
|
||||
// insert_division: {
|
||||
// name: '插入分部',
|
||||
// callback(this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// // const row = this.getSelectedLast?.()[0]
|
||||
// // const hotInstance = hstRef.value.hotInstance
|
||||
// const row = this.countRows()
|
||||
// console.log('row', row)
|
||||
// this.alter('insert_row_below', row, 1);
|
||||
// this.setDataAtRowProp([
|
||||
// [row, 'nodeType', 'division'],
|
||||
// [row, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, 'division')],
|
||||
// [row, 'name', '新建分部'],
|
||||
// [row, 'tabType', 'division'],
|
||||
// [row, 'code', String(new Date().getTime())],
|
||||
// ], 'insertRow')
|
||||
// }
|
||||
// },
|
||||
// insert_child_division: {
|
||||
// name: '插入子分部',
|
||||
// disabled(this: any): boolean {
|
||||
// // 只在分部行上可用
|
||||
// const selected = this.getSelectedLast?.()
|
||||
// const row = selected[0]
|
||||
// const rowData = this.getSourceDataAtRow(row)
|
||||
// return rowData?.nodeType !== 'division'
|
||||
// },
|
||||
// callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// const hotInstance = hstRef.value.hotInstance
|
||||
// const selected = this.getSelectedLast?.()
|
||||
// const row = selected[0]
|
||||
// const rowData = this.getSourceDataAtRow(row)
|
||||
// // // 子分部的父节点就是当前选中的分部
|
||||
// const parentId = rowData?.id
|
||||
|
||||
// // // 先调用API创建节点
|
||||
// const data = {
|
||||
// catalogItemId: hstRef.value.catalogItemId,
|
||||
// parentId: parentId,
|
||||
// nodeType: 'division',
|
||||
// name: '新建子分部',
|
||||
// tabType: 'division',
|
||||
// code: String(new Date().getTime())
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const id = await createDivisionTemplate(data)
|
||||
// console.log('插入子分部: API创建成功,ID:', data)
|
||||
|
||||
// // // 创建成功后,作为选中分部的子节点插入
|
||||
// hstRef?.value.addChild().then((res: any) => {
|
||||
// // // 创建成功后,在表格中插入新行
|
||||
// hotInstance.setDataAtRowProp([
|
||||
// [res, 'id', id],
|
||||
// [res, 'nodeType', data.nodeType],
|
||||
// [res, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
// [res, 'name', data.name],
|
||||
// [res, 'parentId', data.parentId],
|
||||
// [res, 'code', data.code],
|
||||
// ], 'created')
|
||||
// })
|
||||
// } catch (err) {
|
||||
// console.error('插入子分部: API创建失败', err)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// insert_boq: {
|
||||
// name: '插入清单',
|
||||
// callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// const hotInstance = hstRef.value.hotInstance
|
||||
// const selected = this.getSelectedLast?.()
|
||||
// const row = selected[0]
|
||||
// const rowData = this.getSourceDataAtRow(row)
|
||||
|
||||
// // console.log('插入清单: 选中行', row, '节点类型', rowData?.nodeType, '节点ID', rowData?.id, '父节点ID', rowData?.parentId)
|
||||
|
||||
// // 确定父节点ID
|
||||
// let parentId: any = null
|
||||
// const nodeType = rowData?.nodeType
|
||||
// if (nodeType === 'division') {
|
||||
// parentId = rowData?.id
|
||||
// } else if (nodeType === 'boq') {
|
||||
// parentId = rowData?.parentId
|
||||
// } else if (nodeType === 'quota') {
|
||||
// // const parentBoqData = findParentByType(this, row, 'boq')
|
||||
// // parentId = parentBoqData?.parentId
|
||||
// // operationType = 'below'
|
||||
// //this.alter('insert_row_below', row, 1)
|
||||
// } else {
|
||||
// console.warn('清单只能在根目录或分部节点下创建,当前节点类型:', rowData?.nodeType)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 先调用API创建节点
|
||||
// const data = {
|
||||
// catalogItemId: hstRef.value.catalogItemId,
|
||||
// parentId: parentId,
|
||||
// nodeType: 'boq',
|
||||
// name: '新建清单',
|
||||
// tabType: 'division',
|
||||
// code: String(new Date().getTime())
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const id = await createDivisionTemplate(data)
|
||||
// console.log('插入清单: API创建成功,ID:', data)
|
||||
// if (!id) return;
|
||||
// hstRef?.value.addChild().then((res: any) => {
|
||||
// // 获取父节点对象,统计已有 boq 子节点数量
|
||||
// const plugin = this.getPlugin('nestedRows');
|
||||
// const parentObj = plugin.dataManager.getDataObject(row);
|
||||
// const children = parentObj?.__children || [];
|
||||
// const boqCount = children.filter((child: any) => child.nodeType === 'boq').length;
|
||||
// const sortOrder = boqCount + 1;
|
||||
|
||||
// // 创建成功后,在表格中插入新行
|
||||
// hotInstance.setDataAtRowProp([
|
||||
// [res, 'sortOrder', sortOrder],
|
||||
// [res, 'id', id],
|
||||
// [res, 'nodeType', data.nodeType],
|
||||
// [res, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
// [res, 'name', data.name],
|
||||
// [res, 'parentId', parentId],
|
||||
// [res, 'code', data.code],
|
||||
// ], 'created')
|
||||
// })
|
||||
// } catch (err) {
|
||||
// console.error('插入清单: API创建失败', err)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// separator1: '---------',
|
||||
// remove_row: {
|
||||
// name: '删除',
|
||||
// callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// const selected = this.getSelectedLast?.()
|
||||
// const row = selected[0]
|
||||
// const rowData = this.getSourceDataAtRow(row)
|
||||
// if (rowData.id) {
|
||||
// deleteDivisionTemplate(rowData.id)
|
||||
// }
|
||||
// this.alter('remove_row', row, 1);
|
||||
// }
|
||||
// },
|
||||
// col_show: {
|
||||
// name: '列头筛选',
|
||||
// callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// console.log("列头筛选")
|
||||
// if (hstRef.value.hotInstance.__onColumnApply) hstRef.value.hotInstance.__onColumnApply()
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// };
|
||||
// export const useHst = (hstRef: Ref<any>) => {
|
||||
// const hstSettings = {
|
||||
// data: [],
|
||||
// colWidths: 100,
|
||||
// columns,
|
||||
// colHeaders: true,
|
||||
// rowHeaders: false,
|
||||
// nestedRows: false,
|
||||
// bindRowsWithHeaders: true,
|
||||
// hiddenColumns: {
|
||||
// copyPasteEnabled: true,
|
||||
// indicators: false,
|
||||
// },
|
||||
// contextMenu: handleContextMenu(hstRef),
|
||||
// outsideClickDeselects: false,
|
||||
// currentRowClassName: 'row-highlight',
|
||||
// afterChange(this: any, changes: any[], source: string) {
|
||||
// if (!changes || source === 'loadData' || source === 'updateId' || source === 'created') return;
|
||||
// const hasRealChange = changes.some((change: any) => {
|
||||
// const [, , oldValue, newValue] = change;
|
||||
// const isEmpty = (v: any) => v === null || v === undefined || v === '';
|
||||
// return oldValue !== newValue && !(isEmpty(oldValue) && isEmpty(newValue));
|
||||
// })
|
||||
// if (!hasRealChange) return;
|
||||
// const row = changes[0][0];
|
||||
// const rowData = this.getSourceDataAtRow(row);
|
||||
// const data = {
|
||||
// ...rowData,
|
||||
// catalogItemId: hstRef?.value?.catalogItemId,
|
||||
// tabType: rowData?.tabType == undefined ? 'division' : rowData?.tabType,
|
||||
// code: rowData?.code == undefined ? String(new Date().getTime()) : rowData?.code,
|
||||
// }
|
||||
// console.log('data', data);
|
||||
|
||||
// if (rowData.id == null) {
|
||||
// createDivisionTemplate(data).then(res => {
|
||||
// this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
// ElMessage.success('新增成功')
|
||||
// }).catch(err => {
|
||||
// console.error('新增失败', err)
|
||||
// })
|
||||
// } else {
|
||||
// updateDivisionTemplate(data).then(res => {
|
||||
// console.log('updateUnitDivisionTemplate', res)
|
||||
// }).catch(err => {
|
||||
// console.error('更新失败', err)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// beforeOnCellContextMenu(event: MouseEvent, coords: any) {
|
||||
// // coords.row < 0 表示右键点击的是列头
|
||||
// contextMenuOnHeader = coords.row < 0
|
||||
// },
|
||||
// beforeContextMenuSetItems(menuItems: any[]) {
|
||||
// const headerColumns = ['col_show']
|
||||
// // 必须原地修改数组,因为 Handsontable runHooks 不使用钩子返回值
|
||||
// const filtered = contextMenuOnHeader
|
||||
// ? menuItems.filter(item => headerColumns.includes(item.key))
|
||||
// : menuItems.filter(item => !headerColumns.includes(item.key))
|
||||
// menuItems.splice(0, menuItems.length, ...filtered)
|
||||
// }
|
||||
// };
|
||||
|
||||
// const hstContextMenuItems = [
|
||||
// {
|
||||
// key: 'insert',
|
||||
// name: '插入行',
|
||||
// callback: async (hotInstance: any) => {
|
||||
// // hstRef.value.nestedRowsLoadData([
|
||||
// // ...hotInstance.getSourceData(),
|
||||
// // {nodeType:'division',category:'分部', name: '新建分部'}
|
||||
// // ]);
|
||||
// // const row = hotInstance.countRows()
|
||||
// // hotInstance.alter('insert_row_below', row, 1);
|
||||
// // hotInstance.setDataAtRowProp([
|
||||
// // [row, 'nodeType', 'division'],
|
||||
// // [row, 'category', '分部'],
|
||||
// // [row, 'name', '新建分部'],
|
||||
// // ], 'insertRow')
|
||||
// if (!hstRef.value.catalogItemId) {
|
||||
// ElMessage.error('请选择左侧专业类别')
|
||||
// return
|
||||
// }
|
||||
// // // 先调用API创建节点
|
||||
// const data = {
|
||||
// catalogItemId: hstRef.value.catalogItemId,
|
||||
// nodeType: 'division',
|
||||
// name: '新建分部',
|
||||
// tabType: 'division',
|
||||
// code: String(new Date().getTime())
|
||||
// }
|
||||
// try {
|
||||
// const id = await createDivisionTemplate(data)
|
||||
// console.log('插入根分部: API创建成功,ID:', data)
|
||||
// const row = hotInstance.countRows()
|
||||
// // // 创建成功后,在表格中插入新行
|
||||
// hotInstance.setDataAtRowProp([
|
||||
// [row, 'id', id],
|
||||
// [row, 'nodeType', data.nodeType],
|
||||
// [row, 'category', getDictLabel(DICT_TYPE.DIVISION_CATEGORY, data.nodeType)],
|
||||
// [row, 'name', data.name],
|
||||
// [row, 'code', data.code],
|
||||
// ], 'created')
|
||||
// } catch (err) {
|
||||
// console.error('插入子分部: API创建失败', err)
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const hstInit = () => {
|
||||
// hstRef.value.nestedRowsLoadData([]);
|
||||
// };
|
||||
|
||||
// return {
|
||||
// columns,
|
||||
// hstInit,
|
||||
// hstSettings,
|
||||
// hstContextMenuItems,
|
||||
// };
|
||||
// };
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
import { LazyLoad } from '#/components/lazy-load';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ElCard, ElSplitter, ElSplitterPanel, ElTabPane, ElTabs } from 'element-plus';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import Workbench from './workbench/index.vue';
|
||||
import { useTree } from './use-tree';
|
||||
|
||||
import Materials from './materials/index.vue';
|
||||
import Division from './division/index.vue';
|
||||
import Measure from './measure/index.vue';
|
||||
import Other from './other/index.vue';
|
||||
import Summary from './summary/index.vue';
|
||||
|
||||
|
||||
const dbTreeRef = ref();
|
||||
const activeTab = ref('workbench');
|
||||
|
||||
const {
|
||||
treeData,
|
||||
selectedNode,
|
||||
rootMenus,
|
||||
nodeMenus,
|
||||
loadCategoryTree,
|
||||
handleTreeNodeEdit,
|
||||
handleTreeNodeClick,
|
||||
} = useTree(dbTreeRef);
|
||||
|
||||
onMounted(() => {
|
||||
loadCategoryTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<DbTree
|
||||
ref="dbTreeRef"
|
||||
:tree-data="treeData"
|
||||
:draggable="false"
|
||||
:default-expanded-level="4"
|
||||
:root-menus="rootMenus"
|
||||
:node-menus="nodeMenus"
|
||||
@node-edit="handleTreeNodeEdit"
|
||||
@node-click="handleTreeNodeClick"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel>
|
||||
<LazyLoad :delay="50">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<ElTabs v-model="activeTab" type="border-card" class="h-full w-full configs-tabs">
|
||||
<ElTabPane label="工作台字段" name="workbench" class="h-full w-full" >
|
||||
<Workbench v-if="activeTab === 'workbench'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="工料机字段" name="materials" class="h-full">
|
||||
<Materials v-if="activeTab === 'materials'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="分部分项" name="division" class="h-full">
|
||||
<Division v-if="activeTab === 'division'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="措施项目" name="measure" class="h-full">
|
||||
<Measure v-if="activeTab === 'measure'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="其他项目" name="other" class="h-full">
|
||||
<Other v-if="activeTab === 'other'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="单位汇总" name="summary" class="h-full">
|
||||
<Summary v-if="activeTab === 'summary'" :selected-node="selectedNode" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</LazyLoad>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.configs-tabs {
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
.el-tabs__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { materialSummaryColumns } from '../configs';
|
||||
import { useHst } from './use-hst';
|
||||
import { getResourceFieldList, batchResourceField } from '#/api/database/interface/config';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
|
||||
const hstRef = ref();
|
||||
|
||||
const { hstInit, hstSettings, hstContextMenuItems } = useHst(hstRef);
|
||||
|
||||
const loadAndFillData = async (catalogItemId: number) => {
|
||||
let res = await getResourceFieldList(catalogItemId);
|
||||
|
||||
if (res.length === 0) {
|
||||
// TODO: 批量创建【工料机字段】后重新获取
|
||||
const list: any[] = materialSummaryColumns.map((item: any, index: number) => ({
|
||||
seqNo: index+1,
|
||||
fieldName: item.title,
|
||||
fieldCode: item.data,
|
||||
visible: false,
|
||||
remark: item.remark || '',
|
||||
catalogItemId: catalogItemId
|
||||
}));
|
||||
// console.log('list',list)
|
||||
const batch = await batchResourceField(list)
|
||||
res = await getResourceFieldList(catalogItemId);
|
||||
}
|
||||
|
||||
hstRef.value.catalogItemId = catalogItemId;
|
||||
hstRef.value.hotInstance.loadData(res);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (props.selectedNode) {
|
||||
loadAndFillData(props.selectedNode.id);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedNode,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadAndFillData(val.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="hstSettings" :contextMenuItems="hstContextMenuItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { createResourceField, deleteResourceField, updateResourceField } from '#/api/database/interface/config';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
let contextMenuOnHeader = false
|
||||
|
||||
const columns = [
|
||||
{ type: 'text', data: 'seqNo', title: '序号', width: 60, className: 'htCenter' },
|
||||
{ type: 'text', data: 'fieldName', title: '字段名称' },
|
||||
{ type: 'text', data: 'fieldCode', title: '字段' },
|
||||
{ type: 'checkbox', data: 'visible', title: '隐藏', className: 'htCenter' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
];
|
||||
|
||||
const contextMenuSettings = {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
remove_row: {
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
if(rowData.id){
|
||||
deleteResourceField(rowData.id)
|
||||
}
|
||||
this.alter('remove_row', row, 1);
|
||||
}
|
||||
},
|
||||
col_field_en: {
|
||||
name(this: any): any{
|
||||
const hiddenColumnsPlugin = this.getPlugin('hiddenColumns');
|
||||
return ( hiddenColumnsPlugin.isHidden(2)?'显示':'隐藏')+'字段-英文'
|
||||
},
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const hiddenColumnsPlugin = this.getPlugin('hiddenColumns');
|
||||
hiddenColumnsPlugin.isHidden(2) ? hiddenColumnsPlugin.showColumns([2]) : hiddenColumnsPlugin.hideColumns([2])
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const useHst = (hstRef: Ref<any>) => {
|
||||
const hstSettings = {
|
||||
data: [],
|
||||
colWidths: 150,
|
||||
columns,
|
||||
colHeaders: true,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
hiddenColumns: {
|
||||
columns: [2]
|
||||
},
|
||||
contextMenu: contextMenuSettings,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
afterChange(this: any, changes: any[], source: string) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return;
|
||||
|
||||
const hasRealChange = changes.some((change: any) => {
|
||||
const [, , oldValue, newValue] = change;
|
||||
return oldValue !== newValue && newValue != '';
|
||||
})
|
||||
if (!hasRealChange) return;
|
||||
const row = changes[0][0];
|
||||
const rowData = this.getSourceDataAtRow(row);
|
||||
const data = {...rowData, catalogItemId: hstRef?.value?.catalogItemId}
|
||||
|
||||
if (rowData.id == null) {
|
||||
createResourceField(data).then(res => {
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateResourceField(data).then(res => {
|
||||
console.log('updateUnitResourceField', res)
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeOnCellContextMenu(event: MouseEvent, coords: any) {
|
||||
contextMenuOnHeader = coords.row < 0
|
||||
},
|
||||
beforeContextMenuSetItems(menuItems: any[]) {
|
||||
const headerColumns = ['col_field_en']
|
||||
const filtered = contextMenuOnHeader
|
||||
? menuItems.filter(item => headerColumns.includes(item.key))
|
||||
: menuItems.filter(item => !headerColumns.includes(item.key))
|
||||
menuItems.splice(0, menuItems.length, ...filtered)
|
||||
}
|
||||
};
|
||||
|
||||
const hstContextMenuItems = [
|
||||
{
|
||||
key: 'insert',
|
||||
name: '插入新字段',
|
||||
callback: (hotInstance: any) => {
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1);
|
||||
},
|
||||
},
|
||||
];
|
||||
const hstInit = async () => {
|
||||
}
|
||||
|
||||
return {
|
||||
hstInit,
|
||||
hstSettings,
|
||||
hstContextMenuItems,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Reference from '../components/reference/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Reference ref="referenceRef" :selectedNode="props.selectedNode" tabType="measure" />
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,145 @@
|
||||
// import type { Ref } from 'vue';
|
||||
// import { workbenchColumns } from '../configs';
|
||||
// import { saveUnitTabRefs } from '#/api/database/interface/config';
|
||||
|
||||
// const columns: any[] = [
|
||||
// { type: 'checkbox', data: 'selected', title: '选择', className: 'htCenter', width: '60' },
|
||||
// ...workbenchColumns.map((item: any) => ({
|
||||
// ...item,
|
||||
// width: 100,
|
||||
// }))
|
||||
// ]
|
||||
|
||||
|
||||
// const handleContextMenu = (hstRef: Ref<any>) => {
|
||||
// return {
|
||||
// items: {
|
||||
// // row_above: {},
|
||||
// // row_below: {},
|
||||
// // remove_row: {},
|
||||
// col_show: {
|
||||
// name: '列头筛选',
|
||||
// callback: async function (key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// console.log("列头筛选")
|
||||
// if(hstRef.value.hotInstance.__onColumnApply) hstRef.value.hotInstance.__onColumnApply()
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// };
|
||||
|
||||
// export const useHst = (hstRef: Ref<any>) => {
|
||||
|
||||
// /**
|
||||
// * 递归设置所有子节点的选中状态
|
||||
// */
|
||||
// const setChildrenSelected = (node: any, selected: boolean): void => {
|
||||
// if (!node.__children?.length) return;
|
||||
|
||||
// node.__children.forEach((child: any) => {
|
||||
// child.selected = selected;
|
||||
// setChildrenSelected(child, selected);
|
||||
// });
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 递归检查并更新父节点的选中状态
|
||||
// * 只要有子节点选中,父节点就选中
|
||||
// */
|
||||
// const updateParentSelected = (node: any, dataManager: any): void => {
|
||||
// const parent = dataManager.getRowParent(node);
|
||||
// if (!parent) return;
|
||||
|
||||
// // 检查是否有子节点被选中
|
||||
// parent.selected = parent.__children?.some((child: any) => child.selected) ?? false;
|
||||
|
||||
// // 递归向上更新父节点
|
||||
// updateParentSelected(parent, dataManager);
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 处理 checkbox 变化事件
|
||||
// */
|
||||
// const handleCheckboxChange = (changes: any[] | null, source: string): void => {
|
||||
// if (!changes || source === 'loadData') return;
|
||||
|
||||
// const hotInstance = hstRef.value?.hotInstance;
|
||||
// if (!hotInstance) return;
|
||||
|
||||
// const nestedRowsPlugin = hotInstance.getPlugin('nestedRows');
|
||||
// if (!nestedRowsPlugin?.enabled) return;
|
||||
|
||||
// const dataManager = nestedRowsPlugin.dataManager;
|
||||
|
||||
// for (const [row, prop, , newValue] of changes) {
|
||||
// if (prop !== 'selected') continue;
|
||||
|
||||
// const dataObject = dataManager.getDataObject(row);
|
||||
// if (!dataObject) continue;
|
||||
|
||||
// // 如果是父节点,递归设置所有子节点的选中状态
|
||||
// if (dataManager.isParent(row)) {
|
||||
// setChildrenSelected(dataObject, newValue);
|
||||
// }
|
||||
|
||||
// // 向上更新父节点的选中状态
|
||||
// updateParentSelected(dataObject, dataManager);
|
||||
// }
|
||||
|
||||
// hotInstance.render();
|
||||
// const data = hotInstance?.getSourceData() ?? []
|
||||
// const res = data.filter((f:any)=>f.selected).map((item:any)=>item.id)
|
||||
// if(res.length > 0){
|
||||
// saveUnitTabRefs({catalogItemId: hstRef.value?.catalogItemId, tabType: hstRef.value?.tabType, templateNodeIds: res})
|
||||
// }
|
||||
// console.log(res)
|
||||
// };
|
||||
// const hstSettings = {
|
||||
// data: [],
|
||||
// colWidths: 100,
|
||||
// columns,
|
||||
// colHeaders: true,
|
||||
// rowHeaders: false,
|
||||
// nestedRows: false,
|
||||
// bindRowsWithHeaders: true,
|
||||
// hiddenColumns: {
|
||||
// copyPasteEnabled: true,
|
||||
// indicators: false,
|
||||
// },
|
||||
// contextMenu: handleContextMenu(hstRef),
|
||||
// // contextMenu: contextMenuSettings,
|
||||
// outsideClickDeselects: false,
|
||||
// currentRowClassName: 'row-highlight',
|
||||
// afterChange: handleCheckboxChange,
|
||||
// // afterChange(changes: any[], source: string) {
|
||||
// // if (!changes || source === 'loadData' || source === 'updateId') return;
|
||||
|
||||
// // const row = changes[0][0];
|
||||
// // const rowData = this.getSourceDataAtRow(row);
|
||||
|
||||
// // console.log('measure afterChange', rowData);
|
||||
// // },
|
||||
// };
|
||||
|
||||
// const hstContextMenuItems: any[] = [
|
||||
// // {
|
||||
// // key: 'insert',
|
||||
// // name: '引用-分部分项',
|
||||
// // callback: (hotInstance: any) => {
|
||||
// // // hotInstance.alter('insert_row_below', hotInstance.countRows(), 1);
|
||||
// // if(hstRef.value.hotInstance.__onReference) hstRef.value.hotInstance.__onReference()
|
||||
// // },
|
||||
// // },
|
||||
// ];
|
||||
|
||||
// const hstInit = () => {
|
||||
// hstRef.value.hotInstance.loadData([]);
|
||||
// };
|
||||
|
||||
// return {
|
||||
// columns,
|
||||
// hstInit,
|
||||
// hstSettings,
|
||||
// hstContextMenuItems,
|
||||
// };
|
||||
// };
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Reference from '../components/reference/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Reference ref="referenceRef" :selectedNode="props.selectedNode" tabType="other"/>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,63 @@
|
||||
// import type { Ref } from 'vue';
|
||||
// import { workbenchColumns } from '../configs';
|
||||
|
||||
// const columns: any[] = workbenchColumns.map((item: any) => ({
|
||||
// type: 'text',
|
||||
// data: item.data,
|
||||
// title: item.title,
|
||||
// width: 100,
|
||||
// }));
|
||||
|
||||
// const contextMenuSettings = {
|
||||
// items: {
|
||||
// row_above: {},
|
||||
// row_below: {},
|
||||
// remove_row: {},
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const useHst = (hstRef: Ref<any>) => {
|
||||
// const hstSettings = {
|
||||
// data: [],
|
||||
// colWidths: 100,
|
||||
// columns,
|
||||
// rowHeaders: false,
|
||||
// nestedRows: false,
|
||||
// bindRowsWithHeaders: true,
|
||||
// hiddenColumns: {
|
||||
// copyPasteEnabled: true,
|
||||
// indicators: false,
|
||||
// },
|
||||
// contextMenu: contextMenuSettings,
|
||||
// outsideClickDeselects: false,
|
||||
// currentRowClassName: 'row-highlight',
|
||||
// afterChange(changes: any[], source: string) {
|
||||
// if (!changes || source === 'loadData' || source === 'updateId') return;
|
||||
|
||||
// const row = changes[0][0];
|
||||
// const rowData = this.getSourceDataAtRow(row);
|
||||
|
||||
// console.log('other afterChange', rowData);
|
||||
// },
|
||||
// };
|
||||
|
||||
// const hstContextMenuItems = [
|
||||
// {
|
||||
// key: 'insert',
|
||||
// name: '插入行',
|
||||
// callback: (hotInstance: any) => {
|
||||
// hotInstance.alter('insert_row_below', hotInstance.countRows(), 1);
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const hstInit = () => {
|
||||
// hstRef.value.hotInstance.loadData([]);
|
||||
// };
|
||||
|
||||
// return {
|
||||
// hstInit,
|
||||
// hstSettings,
|
||||
// hstContextMenuItems,
|
||||
// };
|
||||
// };
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Reference from '../components/reference/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Reference ref="referenceRef" :selectedNode="props.selectedNode" tabType="unit_summary"/>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,89 @@
|
||||
// import type { Ref } from 'vue';
|
||||
// import { unitSummary, workbenchColumns } from '../configs';
|
||||
|
||||
// // const columns = [
|
||||
// // { type: 'db.nestedRows', data: 'number', title: '序号', width: 160 },
|
||||
// // { type: 'text', data: 'name', title: '名称', width: 200 },
|
||||
// // { type: 'text', data: 'foundation', title: '计算基础'},
|
||||
// // { type: 'numeric', data: 'rate', title: '费率'},
|
||||
// // { type: 'numeric', data: 'total', title: '合价'},
|
||||
// // ];
|
||||
|
||||
// const columns: any[] = workbenchColumns.map((item: any) => ({
|
||||
// type: item.type,
|
||||
// data: item.data,
|
||||
// title: item.title,
|
||||
// width: 100,
|
||||
// }));
|
||||
|
||||
// const contextMenuSettings = {
|
||||
// items: {
|
||||
// row_above: {},
|
||||
// row_below: {},
|
||||
// remove_row: {},
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const useHst = (hstRef: Ref<any>) => {
|
||||
// const hstSettings = {
|
||||
// data: [],
|
||||
// colWidths: 150,
|
||||
// columns,
|
||||
// rowHeaders: false,
|
||||
// nestedRows: false,
|
||||
// bindRowsWithHeaders: true,
|
||||
// hiddenColumns: {
|
||||
// copyPasteEnabled: true,
|
||||
// indicators: false,
|
||||
// },
|
||||
// contextMenu: contextMenuSettings,
|
||||
// outsideClickDeselects: false,
|
||||
// currentRowClassName: 'row-highlight',
|
||||
// afterChange(changes: any[], source: string) {
|
||||
// if (!changes || source === 'loadData' || source === 'updateId') return;
|
||||
|
||||
// const row = changes[0][0];
|
||||
// const rowData = this.getSourceDataAtRow(row);
|
||||
|
||||
// console.log('supplement afterChange', rowData);
|
||||
// },
|
||||
// };
|
||||
|
||||
// const hstContextMenuItems = [
|
||||
// {
|
||||
// key: 'insert',
|
||||
// name: '插入行',
|
||||
// callback: (hotInstance: any) => {
|
||||
// hotInstance.alter('insert_row_below', hotInstance.countRows(), 1);
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const hstInit = () => {
|
||||
// // const toRow = (item: any, num: number | string) => ({
|
||||
// // number: num, name: item.title, data: item.data, foundation: '', rate: null, total: null,
|
||||
// // });
|
||||
// // const childrenMap = unitSummary
|
||||
// // .filter((c: any) => c._parentId != null)
|
||||
// // .reduce((m: Map<number, any[]>, c: any) => m.set(c._parentId, [...(m.get(c._parentId) || []), c]), new Map<number, any[]>());
|
||||
|
||||
// // let i = 0;
|
||||
// // const data = unitSummary
|
||||
// // .filter((p: any) => p._id != null)
|
||||
// // .map((p: any) => {
|
||||
// // const parentNum = ++i;
|
||||
// // const row: any = toRow(p, parentNum);
|
||||
// // const children = childrenMap.get(p._id);
|
||||
// // if (children?.length) row.__children = children.map((c: any, ci: number) => toRow(c, `${parentNum}.${ci + 1}`));
|
||||
// // return row;
|
||||
// // });
|
||||
|
||||
// hstRef.value.nestedRowsLoadData([]);
|
||||
// };
|
||||
|
||||
// return {
|
||||
// hstInit,
|
||||
// hstSettings,
|
||||
// hstContextMenuItems,
|
||||
// };
|
||||
// };
|
||||
166
apps/web-ele/src/views/database/interface/unitConfig/tree.ts
Normal file
166
apps/web-ele/src/views/database/interface/unitConfig/tree.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// import {
|
||||
// createCatalogNode,
|
||||
// deleteCatalogNode,
|
||||
// type CardinalRateApi,
|
||||
// } from '#/api/database/interface/cardinalRate';
|
||||
import {
|
||||
createQuotaCatalogItem,
|
||||
deleteQuotaCatalogItem
|
||||
} from '#/api/database/quota/index';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
type MenuCallback = (command: string, node: any, data: any, treeInstance: any) => Promise<void>;
|
||||
|
||||
type MenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
divided?: boolean;
|
||||
callback: MenuCallback;
|
||||
};
|
||||
|
||||
type NodeConfig = {
|
||||
nodeType: string;
|
||||
name: string;
|
||||
codePrefix: string;
|
||||
};
|
||||
|
||||
// 生成唯一代码
|
||||
const generateCode = (prefix: string): string => `${prefix}-${Date.now()}`;
|
||||
|
||||
// 节点配置
|
||||
const ROOT_CONFIG: NodeConfig = { nodeType: 'root', name: '根节点', codePrefix: 'ROOT' };
|
||||
const PROVINCE_CONFIG: NodeConfig = { nodeType: 'province', name: '省市', codePrefix: 'PROVINCE' };
|
||||
const CONTENT_CONFIG: NodeConfig = { nodeType: 'content', name: '清单', codePrefix: 'CONTENT' };
|
||||
const FIELDS_MAJORS_CONFIG: NodeConfig = { nodeType: 'fields_majors', name: '专业类别', codePrefix: 'FIELDS_MAJORS' };// 就是字段配置
|
||||
|
||||
// 创建菜单项
|
||||
const createMenuItem = (key: string, label: string, callback: MenuCallback, divided?: boolean): MenuItem => ({
|
||||
key,
|
||||
label,
|
||||
callback,
|
||||
...(divided && { divided }),
|
||||
});
|
||||
|
||||
// 添加根节点回调
|
||||
const createAddRootCallback = (config: NodeConfig): MenuCallback => {
|
||||
return async (_command, _node, _data, treeInstance) => {
|
||||
const source = {
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: treeInstance.getData().length + 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
treeInstance.addData({
|
||||
id: String(res.id),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('添加成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 添加子节点回调
|
||||
const createAddChildCallback = (config: NodeConfig): MenuCallback => {
|
||||
return async (_command, _node, data, treeInstance) => {
|
||||
const source = {
|
||||
parentId: data.id,
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: (data.children?.length || 0) + 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
treeInstance.addData({
|
||||
id: String(res),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('添加成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 插入节点回调
|
||||
const createInsertCallback = (config: NodeConfig, position: 'above' | 'below'): MenuCallback => {
|
||||
return async (_command, node, data, treeInstance) => {
|
||||
const source = {
|
||||
parentId: node.parent?.data?.id || null,
|
||||
code: generateCode(config.codePrefix),
|
||||
name: config.name,
|
||||
nodeType: config.nodeType,
|
||||
sortOrder: data.sortOrder || 1,
|
||||
attributes: {},
|
||||
};
|
||||
const res = await createQuotaCatalogItem(source);
|
||||
const insertMethod = position === 'above' ? 'insertAbove' : 'insertBelow';
|
||||
treeInstance[insertMethod]({
|
||||
id: String(res),
|
||||
label: source.name,
|
||||
children: [],
|
||||
...source,
|
||||
});
|
||||
ElMessage.success('插入成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 删除节点回调
|
||||
const createDeleteCallback = (): MenuCallback => {
|
||||
return async (_command, _node, data, treeInstance) => {
|
||||
await deleteQuotaCatalogItem(data.id);
|
||||
treeInstance.removeData();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
};
|
||||
|
||||
// 根节点菜单 - 空树时显示
|
||||
export const rootMenus = [
|
||||
// {
|
||||
// key: 'add-root',
|
||||
// label: '添加根节点',
|
||||
// callback: createAddRootCallback(ROOT_CONFIG),
|
||||
// },
|
||||
];
|
||||
|
||||
// 节点层级菜单
|
||||
export const nodeMenus = [
|
||||
// {
|
||||
// level: 1, // 根节点层级
|
||||
// items: [
|
||||
// createMenuItem('add-province', '添加省市', createAddChildCallback(PROVINCE_CONFIG)),
|
||||
// createMenuItem('remove-root', '删除', createDeleteCallback(), true),
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// level: 2, // 省市节点层级
|
||||
// items: [
|
||||
// createMenuItem('add-province-above', '上方添加省市', createInsertCallback(PROVINCE_CONFIG, 'above')),
|
||||
// createMenuItem('add-province-below', '下方添加省市', createInsertCallback(PROVINCE_CONFIG, 'below')),
|
||||
// createMenuItem('add-content', '添加清单', createAddChildCallback(CONTENT_CONFIG)),
|
||||
// createMenuItem('remove-province', '删除', createDeleteCallback(), true),
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
level: 3, // 内容节点层级(叶子节点)
|
||||
items: [
|
||||
// createMenuItem('add-content-above', '上方添加清单', createInsertCallback(CONTENT_CONFIG, 'above')),
|
||||
// createMenuItem('add-content-below', '下方添加清单', createInsertCallback(CONTENT_CONFIG, 'below')),
|
||||
|
||||
createMenuItem('add-fields-majors', '添加专业类别', createAddChildCallback(FIELDS_MAJORS_CONFIG)),
|
||||
// createMenuItem('remove-content', '删除', createDeleteCallback(), true),
|
||||
],
|
||||
},
|
||||
{
|
||||
level: 4, // 内容节点层级(叶子节点)
|
||||
items: [
|
||||
createMenuItem('add-fields-majors-above', '上方添加专业类别', createInsertCallback(FIELDS_MAJORS_CONFIG, 'above')),
|
||||
createMenuItem('add-fields-majors-below', '下方添加专业类别', createInsertCallback(FIELDS_MAJORS_CONFIG, 'below')),
|
||||
|
||||
createMenuItem('remove-fields-majors', '删除', createDeleteCallback(), true),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { getQuotaCatalogItemTree, updateQuotaCatalogItem } from '#/api/database/quota/index';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { nodeMenus, rootMenus } from './tree';
|
||||
|
||||
const transformTreeData = (data: any[]): any[] => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn('transformTreeData: 数据不是数组', data);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
const treeNode = {
|
||||
...item,
|
||||
id: String(item.id),
|
||||
label: item.name || item.label || '未命名',
|
||||
};
|
||||
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
treeNode.children = transformTreeData(item.children);
|
||||
}
|
||||
|
||||
return treeNode;
|
||||
});
|
||||
};
|
||||
|
||||
export const useTree = (_dbTreeRef: Ref<any>) => {
|
||||
const treeData = ref<any[]>([]);
|
||||
const selectedNode = ref<any>(null);
|
||||
|
||||
const loadCategoryTree = async () => {
|
||||
try {
|
||||
const res = await getQuotaCatalogItemTree({ exclude: 'rate_mode,majors' });
|
||||
console.log('定额专业树原始数据:', res);
|
||||
treeData.value = transformTreeData(res);
|
||||
console.log('定额专业树转换后数据:', treeData.value);
|
||||
} catch (error) {
|
||||
console.error('加载定额专业树失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreeNodeEdit = async (payload: any) => {
|
||||
const { data, newLabel } = payload;
|
||||
console.log('节点编辑保存:', payload);
|
||||
|
||||
await updateQuotaCatalogItem({
|
||||
id: data.id,
|
||||
code: data.code,
|
||||
name: newLabel,
|
||||
});
|
||||
ElMessage.success('更新成功');
|
||||
};
|
||||
|
||||
const handleTreeNodeClick = (payload: any) => {
|
||||
const { data } = payload;
|
||||
console.log('节点点击:', payload);
|
||||
|
||||
if (data.nodeType === 'fields_majors') {
|
||||
selectedNode.value = { ...data };//为了每次watch都能监听到,不管new/old
|
||||
} else {
|
||||
selectedNode.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
treeData,
|
||||
selectedNode,
|
||||
rootMenus,
|
||||
nodeMenus,
|
||||
loadCategoryTree,
|
||||
handleTreeNodeEdit,
|
||||
handleTreeNodeClick,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { DbHst } from '#/components/db-hst';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useHst } from './use-hst';
|
||||
import { getFieldConfigList, batchCreateFieldConfig } from '#/api/database/interface/config';
|
||||
import { workbenchColumns } from '../configs';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode?: any;
|
||||
}>();
|
||||
|
||||
const hstRef = ref();
|
||||
|
||||
const { hstInit, hstSettings, hstContextMenuItems } = useHst(hstRef);
|
||||
|
||||
const loadAndFillData = async (catalogItemId: number) => {
|
||||
let res = await getFieldConfigList(catalogItemId);
|
||||
|
||||
if (res.length === 0) {
|
||||
// TODO: 批量创建【工作台字段】后重新获取
|
||||
//批量创建默认【工作台字段】
|
||||
const list: any[] = workbenchColumns.map((item: any, index: number) => ({
|
||||
seqNo: index+1,
|
||||
fieldName: item.title,
|
||||
fieldCode: item.data,
|
||||
divisionsHidden: false,
|
||||
measureHidden: false,
|
||||
otherHidden: false,
|
||||
summaryHidden: false,
|
||||
remark: item.remark || '',
|
||||
catalogItemId: catalogItemId
|
||||
}));
|
||||
const batch = await batchCreateFieldConfig(list)
|
||||
// console.log('list',list, batch)
|
||||
//再次获取
|
||||
res = await getFieldConfigList(catalogItemId);
|
||||
}
|
||||
|
||||
hstRef.value.catalogItemId = catalogItemId;
|
||||
hstRef.value.hotInstance.loadData(res);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (props.selectedNode) {
|
||||
loadAndFillData(props.selectedNode.id);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedNode,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadAndFillData(val.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<DbHst ref="hstRef" :settings="hstSettings" :contextMenuItems="hstContextMenuItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { createFieldConfig, deleteFieldConfig, updateFieldConfig } from '#/api/database/interface/config';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
let contextMenuOnHeader = false
|
||||
|
||||
const columns = [
|
||||
{ type: 'text', data: 'seqNo', title: '序号', width: 60, className: 'htCenter' },
|
||||
{ type: 'text', data: 'fieldName', title: '字段名称' },
|
||||
{ type: 'text', data: 'fieldCode', title: '字段' },
|
||||
{ type: 'checkbox', data: 'divisionHidden', title: '分部分项隐藏', className: 'htCenter' },
|
||||
{ type: 'checkbox', data: 'measureHidden', title: '措施项目隐藏', className: 'htCenter' },
|
||||
{ type: 'checkbox', data: 'otherHidden', title: '其他项目隐藏', className: 'htCenter' },
|
||||
{ type: 'checkbox', data: 'summaryHidden', title: '汇总分析隐藏', className: 'htCenter' },
|
||||
{ type: 'text', data: 'remark', title: '备注' },
|
||||
];
|
||||
|
||||
const contextMenuSettings = {
|
||||
items: {
|
||||
row_above: {},
|
||||
row_below: {},
|
||||
remove_row: {
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
const selected = this.getSelectedLast?.()
|
||||
const row = selected[0]
|
||||
const rowData = this.getSourceDataAtRow(row)
|
||||
console.log(rowData)
|
||||
if(rowData.id){
|
||||
deleteFieldConfig(rowData.id)
|
||||
}
|
||||
this.alter('remove_row', row, 1);
|
||||
}
|
||||
},
|
||||
col_field_en: {
|
||||
name(this: any): any{
|
||||
const hiddenColumnsPlugin = this.getPlugin('hiddenColumns');
|
||||
return ( hiddenColumnsPlugin.isHidden(2)?'显示':'隐藏')+'字段-英文'
|
||||
},
|
||||
callback: async function (this: any, key: string, selection: any[], clickEvent: MouseEvent) {
|
||||
// console.log("字段-英文")
|
||||
// hstRef.value.handleColumnApply(data)
|
||||
const hiddenColumnsPlugin = this.getPlugin('hiddenColumns');
|
||||
// console.log('hiddenColumnsPlugin.isHidden(2)',hiddenColumnsPlugin.isHidden(2))
|
||||
hiddenColumnsPlugin.isHidden(2) ? hiddenColumnsPlugin.showColumns([2]) : hiddenColumnsPlugin.hideColumns([2])
|
||||
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const useHst = (hstRef: Ref<any>) => {
|
||||
const hstSettings = {
|
||||
data: [],
|
||||
colWidths: 100,
|
||||
columns,
|
||||
colHeaders: true,
|
||||
rowHeaders: false,
|
||||
nestedRows: false,
|
||||
bindRowsWithHeaders: true,
|
||||
hiddenColumns: {
|
||||
// copyPasteEnabled: true,
|
||||
// indicators: true,
|
||||
columns: [2]
|
||||
},
|
||||
contextMenu: contextMenuSettings,
|
||||
outsideClickDeselects: false,
|
||||
currentRowClassName: 'row-highlight',
|
||||
afterChange(this: any, changes: any[], source: string) {
|
||||
if (!changes || source === 'loadData' || source === 'updateId') return;
|
||||
|
||||
const hasRealChange = changes.some((change: any) => {
|
||||
const [, , oldValue, newValue] = change;
|
||||
const isEmpty = (v: any) => v === null || v === undefined || v === '';
|
||||
return oldValue !== newValue && !(isEmpty(oldValue) && isEmpty(newValue));
|
||||
})
|
||||
if (!hasRealChange) return;
|
||||
const row = changes[0][0];
|
||||
const rowData = this.getSourceDataAtRow(row);
|
||||
const data = {...rowData, catalogItemId: hstRef?.value?.catalogItemId}
|
||||
console.log('data', data);
|
||||
|
||||
if (rowData.id == null) {
|
||||
createFieldConfig(data).then(res => {
|
||||
this.setDataAtRowProp(row, 'id', res, 'updateId')
|
||||
ElMessage.success('新增成功')
|
||||
}).catch(err => {
|
||||
console.error('新增失败', err)
|
||||
})
|
||||
} else {
|
||||
updateFieldConfig(data).then(res => {
|
||||
console.log('updateResourceItems', res)
|
||||
}).catch(err => {
|
||||
console.error('更新失败', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeOnCellContextMenu(event: MouseEvent, coords: any) {
|
||||
// coords.row < 0 表示右键点击的是列头
|
||||
contextMenuOnHeader = coords.row < 0
|
||||
},
|
||||
beforeContextMenuSetItems(menuItems: any[]) {
|
||||
const headerColumns = ['col_field_en']
|
||||
// 必须原地修改数组,因为 Handsontable runHooks 不使用钩子返回值
|
||||
const filtered = contextMenuOnHeader
|
||||
? menuItems.filter(item => headerColumns.includes(item.key))
|
||||
: menuItems.filter(item => !headerColumns.includes(item.key))
|
||||
menuItems.splice(0, menuItems.length, ...filtered)
|
||||
}
|
||||
};
|
||||
|
||||
const hstContextMenuItems = [
|
||||
{
|
||||
key: 'insert',
|
||||
name: '插入新字段',
|
||||
callback: (hotInstance: any) => {
|
||||
hotInstance.alter('insert_row_below', hotInstance.countRows(), 1);
|
||||
},
|
||||
},
|
||||
];
|
||||
const hstInit = async () => {
|
||||
// const data: any[] = workbenchColumns.map((item: any, index: number) => ({
|
||||
// ...{ number: index + 1, fieldName: item.title, fieldCode: item.data, divisionsHidden: false, measureHidden: false, otherHidden: false, summaryHidden: false, remark: item.remark || '' }
|
||||
// }));
|
||||
// // console.log('data', data);
|
||||
// hstRef.value.hotInstance.loadData(data);
|
||||
}
|
||||
|
||||
return {
|
||||
hstInit,
|
||||
hstSettings,
|
||||
hstContextMenuItems,
|
||||
};
|
||||
};
|
||||
88
apps/web-ele/src/views/database/interface/variable.vue
Normal file
88
apps/web-ele/src/views/database/interface/variable.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<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,ElCard,ElTabs,ElTabPane } from 'element-plus';
|
||||
import { DbTree } from '#/components/db-tree';
|
||||
|
||||
// 导入子组件
|
||||
import VariableSettings from './unit/VariableSettings.vue'
|
||||
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 categoryHandleSelect = (payload: { node: any, data: any, event: MouseEvent }) => {
|
||||
const { node, data, event } = payload
|
||||
console.log('categoryhandleSelect',node, data, event)
|
||||
}
|
||||
|
||||
const detailHandleSelect = (node: Tree) => {
|
||||
// if (topHstRef.value && typeof topHstRef.value.loadData === 'function') {
|
||||
// // console.log('hstData.value',hstData.value)
|
||||
// // topHstRef.value.loadData(topHstData.value)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="15%" :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full flex flex-col">
|
||||
<DbTree
|
||||
ref="detailTreeRef"
|
||||
:tree-data="categoryTreeData"
|
||||
@node-click="categoryHandleSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<ElCard class="w-full h-full border-radius-0" body-class="!p-0 h-full w-full">
|
||||
<VariableSettings />
|
||||
</ElCard>
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.el-tabs--border-card>.el-tabs__content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user