581 lines
18 KiB
Vue
581 lines
18 KiB
Vue
<template>
|
|
<j-data-table
|
|
v-if="!heavyLoad"
|
|
ref="tableRef"
|
|
:data-source="dataSource"
|
|
:columns="columns"
|
|
:height="560"
|
|
:searchProps="{
|
|
placeholder: '请输入搜索名称'
|
|
}"
|
|
serial
|
|
@editStatus="editStatusChange"
|
|
@change="(data) => dataSourceCache = data"
|
|
>
|
|
<template #expand>
|
|
<!-- <PermissionButton
|
|
type="primary"
|
|
v-if="!showSave"
|
|
:hasPermission="`${permission}:update`"
|
|
key="add"
|
|
|
|
:disabled="hasOperate('add', type)"
|
|
:tooltip="{
|
|
placement: hasOperate('add', type) ? 'topRight' : 'top',
|
|
title: hasOperate('add', type)
|
|
? '当前的存储方式不支持新增'
|
|
: '新增',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
@click="handleAddClick()"
|
|
placement="topRight"
|
|
>
|
|
新增
|
|
</PermissionButton> -->
|
|
<PermissionButton
|
|
type="primary"
|
|
:hasPermission="`${permission}:update`"
|
|
key="update"
|
|
:loading="loading"
|
|
:disabled="hasOperate('add', type) || !editStatus"
|
|
:tooltip="{
|
|
title: hasOperate('add', type)
|
|
? '当前的存储方式不支持新增'
|
|
: !editStatus ? '暂无改动数据': '保存',
|
|
placement: hasOperate('add', type) ? 'topRight' : 'top',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
@click="handleSaveClick()"
|
|
placement="topRight"
|
|
>
|
|
保存
|
|
</PermissionButton>
|
|
</template>
|
|
<template #valueType="{ data }">
|
|
{{ TypeStringMap[data.record.valueType?.type] }}
|
|
</template>
|
|
<template #inputs="{ data }">
|
|
<j-tooltip
|
|
v-if="target === 'device' && productNoEdit.id?.includes?.(data.record.id)"
|
|
title="继承自产品物模型的数据不支持修改"
|
|
>
|
|
<!-- <ModelButton :disabled="true"/>-->
|
|
<j-button :disabled="true" type="link" >
|
|
<AIcon type="SettingOutlined" />
|
|
配置
|
|
</j-button>
|
|
|
|
</j-tooltip>
|
|
<InputParams
|
|
v-else
|
|
v-model:value="data.record.inputs"
|
|
:has-permission="`${permission}:update`"
|
|
/>
|
|
</template>
|
|
<template #output="{ data }">
|
|
{{ data.record.output?.type }}
|
|
</template>
|
|
<template #async="{ data }">
|
|
{{ data.record.async ? '是' : '否' }}
|
|
</template>
|
|
<template #expands="{ data }" v-if="type === 'events'">
|
|
{{ levelMap?.[data.record.expands?.level] || '-' }}
|
|
</template>
|
|
<template v-else-if="type === 'properties'" #expands="{ data }">
|
|
{{ data.record.id && !data.record?.expands?.source ? '设备' : sourceMap?.[data.record?.expands?.source] || '' }}
|
|
</template>
|
|
<template #properties="{ data }">
|
|
<j-tooltip
|
|
v-if="target === 'device' && productNoEdit.id?.includes?.(data.record.id)"
|
|
title="继承自产品物模型的数据不支持修改"
|
|
>
|
|
<!-- <ModelButton :disabled="true"/>-->
|
|
<j-button :disabled="true" type="link">
|
|
<AIcon type="SettingOutlined" />
|
|
配置
|
|
</j-button>
|
|
</j-tooltip>
|
|
<ConfigParams
|
|
v-else
|
|
v-model:value="data.record.valueType"
|
|
:has-permission="`${permission}:update`"
|
|
/>
|
|
</template>
|
|
<template #outInput>
|
|
object
|
|
</template>
|
|
<template #readType="{data}">
|
|
<j-tag v-for="item in data.record?.expands?.type || []" :key="item">
|
|
{{ expandsType[item] }}
|
|
</j-tag>
|
|
</template>
|
|
<template #other="{ data }">
|
|
<!-- <j-tooltip
|
|
v-if="target === 'device' && productNoEdit.id?.includes?.(data.record.id)"
|
|
title="继承自产品物模型的数据不支持修改"
|
|
> -->
|
|
<!-- <ModelButton :disabled="true"/>-->
|
|
<!-- <j-button :disabled="true" type="link" style="padding-left: 0;">
|
|
<AIcon type="SettingOutlined" />
|
|
配置
|
|
</j-button> -->
|
|
<!-- </j-tooltip> -->
|
|
<OtherSetting
|
|
v-model:value="data.record.expands"
|
|
:id="data.record.id"
|
|
:disabled="target === 'device' && productNoEdit.id?.includes?.(data.record.id)"
|
|
:record="data.record"
|
|
:type="data.record.valueType.type"
|
|
:has-permission="`${permission}:update`"
|
|
:tooltip="target === 'device' && productNoEdit.id?.includes?.(data.record.id) ? {
|
|
title: '继承自产品物模型的数据不支持删除',
|
|
} : undefined"
|
|
/>
|
|
|
|
</template>
|
|
<template #action="{data}">
|
|
<j-space>
|
|
<PermissionButton
|
|
:has-permission="`${permission}:update`"
|
|
type="link"
|
|
key="edit"
|
|
style="padding: 0"
|
|
:disabled="!!operateLimits('add', type)"
|
|
@click="copyItem(data.record, data.index)"
|
|
:tooltip="{
|
|
title: operateLimits('add', type) ? '当前的存储方式不支持复制' : '复制',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
>
|
|
<AIcon type="CopyOutlined" />
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
:has-permission="`${permission}:update`"
|
|
type="link"
|
|
key="edit"
|
|
style="padding: 0"
|
|
:disabled="!!operateLimits('add', type)"
|
|
@click="handleAddClick(null, data.index)"
|
|
:tooltip="{
|
|
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
>
|
|
<AIcon type="PlusSquareOutlined" />
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
:has-permission="true"
|
|
type="link"
|
|
key="edit"
|
|
style="padding: 0"
|
|
@click="showDetail(data.record)"
|
|
:tooltip="{
|
|
title: '详情',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
>
|
|
<AIcon type="FileSearchOutlined" />
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
:has-permission="`${permission}:update`"
|
|
type="link"
|
|
key="delete"
|
|
style="padding: 0"
|
|
danger
|
|
:pop-confirm="{
|
|
placement: 'topRight',
|
|
title: showLastDelete ? '这是最后一条数据了,确认删除?' : '确认删除?',
|
|
onConfirm: async () => {
|
|
await removeItem(data.index);
|
|
},
|
|
getPopupContainer: getPopupContainer
|
|
}"
|
|
:disabled="target === 'device' && productNoEdit.id?.includes?.(data.record.id)"
|
|
:tooltip="{
|
|
placement: 'topRight',
|
|
getPopupContainer: getPopupContainer,
|
|
title: target === 'device' && productNoEdit.id?.includes?.(data.record.id) ? '继承自产品物模型的数据不支持删除' :'删除',
|
|
}"
|
|
>
|
|
<AIcon type="DeleteOutlined" />
|
|
</PermissionButton>
|
|
</j-space>
|
|
</template>
|
|
</j-data-table>
|
|
<PermissionButton
|
|
type="primary"
|
|
block
|
|
ghost
|
|
:hasPermission="`${permission}:update`"
|
|
key="add"
|
|
:disabled="hasOperate('add', type)"
|
|
:tooltip="{
|
|
placement: hasOperate('add', type) ? 'topRight' : 'top',
|
|
title: hasOperate('add', type)
|
|
? '当前的存储方式不支持新增'
|
|
: '新增',
|
|
getPopupContainer: getPopupContainer,
|
|
}"
|
|
@click="handleAddClick()"
|
|
placement="topRight"
|
|
>
|
|
<template #icon><AIcon type="PlusOutlined"/></template>
|
|
新增行
|
|
</PermissionButton>
|
|
<PropertiesModal
|
|
v-if="type === 'properties' && detailData.visible"
|
|
:data="detailData.data"
|
|
:getPopupContainer="getPopupContainer"
|
|
@cancel="cancelDetailModal"
|
|
/>
|
|
<FunctionModal
|
|
v-else-if="type === 'functions' && detailData.visible"
|
|
:data="detailData.data"
|
|
:getPopupContainer="getPopupContainer"
|
|
@cancel="cancelDetailModal"
|
|
/>
|
|
<EventModal
|
|
v-else-if="type === 'events' && detailData.visible"
|
|
:data="detailData.data"
|
|
:getPopupContainer="getPopupContainer"
|
|
@cancel="cancelDetailModal"
|
|
/>
|
|
<TagsModal
|
|
v-else-if="type === 'tags' && detailData.visible"
|
|
:data="detailData.data"
|
|
:getPopupContainer="getPopupContainer"
|
|
@cancel="cancelDetailModal"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts" name="MetadataBase">
|
|
import type {
|
|
MetadataItem,
|
|
MetadataType,
|
|
ProductItem,
|
|
} from '@/views/device/Product/typings';
|
|
import type { PropType } from 'vue';
|
|
import { TOKEN_KEY } from '@/utils/variable'
|
|
import {useRouter, onBeforeRouteUpdate} from 'vue-router'
|
|
import { useMetadata, useOperateLimits } from './hooks';
|
|
import {TypeStringMap, useColumns} from './columns';
|
|
import { levelMap, sourceMap, expandsType, limitsMap } from './utils';
|
|
import { Source, OtherSetting, InputParams, ConfigParams } from './components';
|
|
import { saveProductVirtualProperty } from '@/api/device/product';
|
|
import { saveDeviceVirtualProperty } from '@/api/device/instance';
|
|
import { useInstanceStore } from '@/store/instance';
|
|
import { useProductStore } from '@/store/product';
|
|
import { asyncUpdateMetadata, updateMetadata } from '../metadata';
|
|
import { useMetadataStore } from '@/store/metadata';
|
|
import { DeviceInstance } from '@/views/device/Instance/typings';
|
|
import { onlyMessage , LocalStore} from '@/utils/comm';
|
|
import {omit} from "lodash-es";
|
|
import { PropertiesModal, FunctionModal, EventModal, TagsModal } from './DetailModal'
|
|
import { Modal } from 'jetlinks-ui-components'
|
|
import {EventEmitter} from "@/utils/utils";
|
|
import {computed, watch} from "vue";
|
|
import {cloneDeep} from "lodash";
|
|
import {useSystem} from "store/system";
|
|
import {storeToRefs} from "pinia";
|
|
import { FULL_CODE } from 'jetlinks-ui-components/es/DataTable'
|
|
import { usePermissionStore } from '@/store/permission';
|
|
import App from '@/App.vue';
|
|
|
|
const props = defineProps({
|
|
target: {
|
|
type: String as PropType<'device' | 'product'>,
|
|
default: 'product',
|
|
},
|
|
type: {
|
|
type: String as PropType<MetadataType>,
|
|
default: undefined,
|
|
},
|
|
permission: {
|
|
type: [String, Array] as PropType<string | string[]>,
|
|
default: undefined,
|
|
},
|
|
});
|
|
|
|
const _target = inject<'device' | 'product'>('_metadataType', props.target);
|
|
|
|
const tableContainer = ref()
|
|
const system = useSystem();
|
|
const {basicLayout} = storeToRefs(system);
|
|
const router = useRouter()
|
|
|
|
const { data: metadata, noEdit, productNoEdit } = useMetadata(_target, props.type);
|
|
const { hasOperate } = useOperateLimits(_target);
|
|
|
|
const permissionStore = usePermissionStore()
|
|
const metadataStore = useMetadataStore()
|
|
const instanceStore = useInstanceStore()
|
|
const productStore = useProductStore()
|
|
|
|
const dataSource = ref<MetadataItem[]>(metadata.value || []);
|
|
const tableRef = ref();
|
|
const loading = ref(false)
|
|
const editStatus = ref(false) // 编辑表格的编辑状态
|
|
|
|
// const columns = computed(() => MetadataMapping.get(props.type!));
|
|
const {columns} = useColumns(props.type, _target, noEdit, productNoEdit)
|
|
|
|
const detailData = reactive({
|
|
data: {},
|
|
visible:false
|
|
})
|
|
|
|
const heavyLoad = ref<Boolean>(false)
|
|
|
|
const showSave = ref(metadata.value.length !== 0)
|
|
|
|
const dataSourceCache = ref<any[]>(metadata.value)
|
|
const fullRef = inject(FULL_CODE);
|
|
|
|
const getPopupContainer = (node: any) => {
|
|
const fullDom = tableRef.value?.fullRef?.()
|
|
return fullDom || node
|
|
}
|
|
|
|
const showLastDelete = computed(() => {
|
|
return dataSourceCache.value.length === 1
|
|
})
|
|
|
|
provide('_dataSource', dataSourceCache)
|
|
const showDetail = (data: any) => {
|
|
detailData.data = data
|
|
detailData.visible = true
|
|
}
|
|
|
|
const cancelDetailModal = () => {
|
|
detailData.data = {}
|
|
detailData.visible = false
|
|
}
|
|
|
|
const operateLimits = (action: 'add' | 'updata', types: MetadataType) => {
|
|
return (
|
|
_target === 'device' &&
|
|
(instanceStore.detail.features || []).find((item: { id: string; name: string }) => item.id === limitsMap.get(`${types}-${action}`))
|
|
);
|
|
};
|
|
|
|
const handleSearch = (searchValue: string) => {
|
|
dataSource.value = searchValue
|
|
? metadata.value
|
|
.filter((item) => item.name!.indexOf(searchValue) > -1)
|
|
.sort((a, b) => b?.sortsIndex - a?.sortsIndex)
|
|
: metadata.value;
|
|
};
|
|
|
|
const getDataByType = () => {
|
|
let _data: any = {
|
|
id: undefined,
|
|
name: undefined,
|
|
expands: {
|
|
source: 'device'
|
|
},
|
|
valueType: {
|
|
type: undefined
|
|
}
|
|
}
|
|
|
|
if (props.type === 'functions') {
|
|
_data = {
|
|
id: undefined,
|
|
name: undefined,
|
|
async: false,
|
|
inputs: [],
|
|
output: {
|
|
type: undefined
|
|
}
|
|
}
|
|
} else if (props.type === 'events') {
|
|
_data = {
|
|
id: undefined,
|
|
name: undefined,
|
|
async: false,
|
|
valueType: {
|
|
type: 'object',
|
|
properties: []
|
|
},
|
|
expands: {
|
|
level: 'ordinary'
|
|
}
|
|
|
|
}
|
|
} else if (props.type === 'tags') {
|
|
_data = {
|
|
id: undefined,
|
|
name: undefined,
|
|
valueType: {
|
|
type: undefined
|
|
},
|
|
expands: {
|
|
type: undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
return _data
|
|
}
|
|
|
|
const handleAddClick = async (_data?: any, index?: number) => {
|
|
|
|
const newObject = _data || getDataByType()
|
|
|
|
const _addData = await tableRef.value.addItem(newObject, index)
|
|
nextTick(()=>{
|
|
if(tableContainer.value.classList.value === 'tableContainer'){
|
|
tableContainer.value.classList.remove('tableContainer')
|
|
}
|
|
})
|
|
// if (_addData.length === 1) {
|
|
// showLastDelete.value = true
|
|
// }
|
|
showSave.value = true
|
|
};
|
|
|
|
const copyItem = (record: any, index: number) => {
|
|
const copyData = cloneDeep(omit(record, ['_uuid', '_sortIndex']))
|
|
copyData.id = `copy_${copyData.id}`
|
|
handleAddClick(copyData, index)
|
|
}
|
|
|
|
const removeItem = (index: number) => {
|
|
// const data = [...dataSource.value];
|
|
// data.splice(index, 1);
|
|
// dataSource.value = data
|
|
const _data = tableRef.value.removeItem(index)
|
|
// if (_data.length === 1) {
|
|
// showLastDelete.value = true
|
|
// }
|
|
if (_data.length === 0) {
|
|
showSave.value = false
|
|
handleSaveClick()
|
|
}
|
|
}
|
|
|
|
const editStatusChange = (status: boolean) => {
|
|
console.log('editStatusChange',status)
|
|
editStatus.value = status
|
|
}
|
|
|
|
const handleSaveClick = async (next?: Function) => {
|
|
let resp = await tableRef.value.getData().finally(() => {
|
|
|
|
});
|
|
if(resp) {
|
|
|
|
const virtual: any[] = [];
|
|
const arr = resp.map((item: any) => {
|
|
if(item.expands?.virtualRule) {
|
|
const triggerProperties = item.expands.virtualRule.triggerProperties
|
|
const rule = omit(item.expands.virtualRule, ['triggerProperties'])
|
|
virtual.push({
|
|
triggerProperties,
|
|
rule,
|
|
type: rule.type,
|
|
propertyId: item.id
|
|
})
|
|
}
|
|
return {
|
|
...item,
|
|
expands: {
|
|
...omit(item.expands, ['virtualRule'])
|
|
}
|
|
}
|
|
// return item
|
|
})
|
|
// 保存规则
|
|
if(virtual.length) {
|
|
let res = undefined
|
|
if(_target === 'device') {
|
|
res = await saveDeviceVirtualProperty(instanceStore.current.productId, instanceStore.current.id, virtual)
|
|
} else {
|
|
res = await saveProductVirtualProperty(productStore.current.id, virtual)
|
|
}
|
|
}
|
|
// 保存属性
|
|
const updateStore = (metadata: string) => {
|
|
if (_target === 'device') {
|
|
const detail = instanceStore.current
|
|
detail.metadata = metadata
|
|
instanceStore.setCurrent(detail)
|
|
} else {
|
|
const detail = productStore.current || {} as ProductItem
|
|
detail.metadata = metadata
|
|
productStore.setCurrent(detail)
|
|
}
|
|
}
|
|
const _detail: ProductItem | DeviceInstance = _target === 'device' ? instanceStore.detail : productStore.current
|
|
let _data = updateMetadata(props.type!, arr, _detail, updateStore)
|
|
loading.value = true
|
|
const result = await asyncUpdateMetadata(_target, _data).finally(() => {
|
|
loading.value = false
|
|
})
|
|
if(result.success) {
|
|
dataSource.value = resp
|
|
tableRef.value.cleanEditStatus()
|
|
editStatus.value = false
|
|
onlyMessage('操作成功!')
|
|
next?.()
|
|
}
|
|
}
|
|
};
|
|
|
|
const tabsChange = inject('tabsChange')
|
|
|
|
const parentTabsChange = (next?: Function) => {
|
|
if (editStatus.value && permissionStore.hasPermission(`${props.permission}:update`) && LocalStore.get(TOKEN_KEY)) {
|
|
const modal = Modal.confirm({
|
|
content: '页面改动数据未保存',
|
|
okText: '保存',
|
|
cancelText: '不保存',
|
|
zIndex: 1400,
|
|
closable: true,
|
|
onOk: () => {
|
|
handleSaveClick(next as Function)
|
|
},
|
|
onCancel: (e: any) => {
|
|
if (!e.triggerCancel) { // 取消按钮
|
|
modal.destroy();
|
|
(next as Function)?.()
|
|
} else {// 右上角取消按钮
|
|
const paths = router.currentRoute.value.matched
|
|
// basicLayout.value.selectedKeys = paths.map(item => item.path)
|
|
basicLayout.value.openKeys = paths.map(item => item.path)
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
(next as Function)?.()
|
|
}
|
|
}
|
|
|
|
EventEmitter.subscribe(['MetadataTabs'], parentTabsChange)
|
|
|
|
onUnmounted(() => {
|
|
EventEmitter.unSubscribe(['MetadataTabs'], parentTabsChange)
|
|
})
|
|
|
|
watch(() => metadata.value, () => {
|
|
dataSource.value = metadata.value
|
|
}, { immediate: true })
|
|
|
|
onBeforeRouteUpdate((to, from, next) => { // 设备管理内路由跳转
|
|
parentTabsChange(next as Function)
|
|
})
|
|
|
|
onBeforeRouteLeave((to, from, next) => { // 设备管理外路由跳转
|
|
parentTabsChange(next as Function)
|
|
})
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding-bottom: 16px;
|
|
}
|
|
</style>
|