Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
6d18f10d2a
|
@ -0,0 +1,12 @@
|
|||
import { patch, post, get } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 列表
|
||||
list: (data: any) => post(`/notifier/config/_query`, data),
|
||||
// 详情
|
||||
detail: (id: string): any => get(`/notifier/config/${id}`),
|
||||
// 新增
|
||||
save: (data: any) => post(`/notifier/config`, data),
|
||||
// 修改
|
||||
update: (data: any) => patch(`/notifier/config`, data)
|
||||
}
|
|
@ -54,8 +54,8 @@
|
|||
delete: item.key === 'delete',
|
||||
}"
|
||||
>
|
||||
<!-- <slot name="actions" v-bind="item"></slot> -->
|
||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||
<slot name="actions" v-bind="item"></slot>
|
||||
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||
<a-button :disabled="item.disabled">
|
||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||
<template v-else>
|
||||
|
@ -72,7 +72,7 @@
|
|||
<span>{{ item.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</template> -->
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
@ -284,13 +284,14 @@ const handleClick = () => {
|
|||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
& > span,
|
||||
button {
|
||||
width: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
& > :deep(span, button) {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
:deep(button) {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
background: #f6f6f6;
|
||||
border: 1px solid #e6e6e6;
|
||||
color: #2f54eb;
|
||||
|
@ -322,7 +323,7 @@ const handleClick = () => {
|
|||
flex-basis: 60px;
|
||||
flex-grow: 0;
|
||||
|
||||
button {
|
||||
:deep(button) {
|
||||
background: @error-color-deprecated-bg;
|
||||
border: 1px solid @error-color-outline;
|
||||
|
||||
|
@ -348,7 +349,7 @@ const handleClick = () => {
|
|||
}
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
:deep(button[disabled]) {
|
||||
background: @disabled-bg;
|
||||
border-color: @disabled-color;
|
||||
|
||||
|
|
|
@ -11,16 +11,21 @@ enum ModelEnum {
|
|||
CARD = 'CARD',
|
||||
}
|
||||
|
||||
enum TypeEnum {
|
||||
TREE = 'TREE',
|
||||
PAGE = 'PAGE',
|
||||
}
|
||||
|
||||
type RequestData = {
|
||||
code: string;
|
||||
result: {
|
||||
data: Record<string, any>[] | undefined;
|
||||
data?: Record<string, any>[] | undefined;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
status: number;
|
||||
} & Record<string, any>;
|
||||
} | Record<string, any>;
|
||||
|
||||
export interface ActionsType {
|
||||
key: string;
|
||||
|
@ -39,16 +44,10 @@ export interface JColumnProps extends ColumnProps{
|
|||
}
|
||||
|
||||
export interface JTableProps extends TableProps{
|
||||
request?: (params: Record<string, any> & {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
}) => Promise<Partial<RequestData>>;
|
||||
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
|
||||
cardBodyClass?: string;
|
||||
columns: JColumnProps[];
|
||||
params?: Record<string, any> & {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
};
|
||||
params?: Record<string, any>;
|
||||
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
||||
// actions?: ActionsType[];
|
||||
noPagination?: boolean;
|
||||
|
@ -64,6 +63,8 @@ export interface JTableProps extends TableProps{
|
|||
*/
|
||||
gridColumns?: number[];
|
||||
alertRender?: boolean;
|
||||
type?: keyof typeof TypeEnum;
|
||||
defaultParams?: Record<string, any>;
|
||||
}
|
||||
|
||||
const JTable = defineComponent<JTableProps>({
|
||||
|
@ -96,10 +97,6 @@ const JTable = defineComponent<JTableProps>({
|
|||
type: [String, undefined],
|
||||
default: undefined
|
||||
},
|
||||
// actions: {
|
||||
// type: Array as PropType<ActionsType[]>,
|
||||
// default: () => []
|
||||
// },
|
||||
noPagination: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
@ -127,6 +124,19 @@ const JTable = defineComponent<JTableProps>({
|
|||
alertRender: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'PAGE'
|
||||
},
|
||||
defaultParams: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
pageIndex: 0,
|
||||
pageSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any,
|
||||
setup(props: JTableProps ,{ slots, emit }){
|
||||
|
@ -162,25 +172,30 @@ const JTable = defineComponent<JTableProps>({
|
|||
const handleSearch = async (_params?: Record<string, any>) => {
|
||||
loading.value = true
|
||||
if(props.request) {
|
||||
const resp = await props.request({
|
||||
pageSize: 12,
|
||||
pageIndex: 1,
|
||||
const resp = await props.request({
|
||||
...props.defaultParams,
|
||||
..._params
|
||||
})
|
||||
if(resp.status === 200){
|
||||
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
||||
if(resp.result?.data?.length === 0 && resp.result.total && resp.result.pageSize && resp.result.pageIndex) {
|
||||
handleSearch({
|
||||
..._params,
|
||||
pageSize: pageSize.value,
|
||||
pageIndex: pageIndex.value - 1,
|
||||
})
|
||||
if(props.type === 'PAGE'){
|
||||
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
||||
if(resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
|
||||
handleSearch({
|
||||
..._params,
|
||||
pageSize: pageSize.value,
|
||||
pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0,
|
||||
})
|
||||
} else {
|
||||
_dataSource.value = resp.result?.data || []
|
||||
pageIndex.value = resp.result?.pageIndex || 0
|
||||
pageSize.value = resp.result?.pageSize || 6
|
||||
total.value = resp.result?.total || 0
|
||||
}
|
||||
} else {
|
||||
_dataSource.value = resp.result?.data || []
|
||||
pageIndex.value = resp.result?.pageIndex || 0
|
||||
pageSize.value = resp.result?.pageSize || 6
|
||||
total.value = resp.result?.total || 0
|
||||
_dataSource.value = resp?.result || []
|
||||
}
|
||||
} else {
|
||||
_dataSource.value = []
|
||||
}
|
||||
} else {
|
||||
_dataSource.value = props?.dataSource || []
|
||||
|
@ -282,7 +297,7 @@ const JTable = defineComponent<JTableProps>({
|
|||
</div>
|
||||
{/* 分页 */}
|
||||
{
|
||||
_dataSource.value.length && !props.noPagination &&
|
||||
(!!_dataSource.value.length) && !props.noPagination &&
|
||||
<div class={styles['jtable-pagination']}>
|
||||
<Pagination
|
||||
size="small"
|
||||
|
@ -292,14 +307,16 @@ const JTable = defineComponent<JTableProps>({
|
|||
current={pageIndex.value}
|
||||
pageSize={pageSize.value}
|
||||
pageSizeOptions={['12', '24', '48', '60', '100']}
|
||||
showTotal={(total, range) => {
|
||||
return `第 ${range[0]} - ${range[1]} 条/总共 ${total} 条`
|
||||
showTotal={(num) => {
|
||||
const minSize = pageIndex.value * pageSize.value + 1;
|
||||
const MaxSize = (pageIndex.value + 1) * pageSize.value;
|
||||
return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
|
||||
}}
|
||||
onChange={(page, size) => {
|
||||
handleSearch({
|
||||
...props.params,
|
||||
pageSize: size,
|
||||
pageIndex: pageSize.value === size ? page : 1,
|
||||
pageIndex: pageSize.value === size ? page : 0
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useMenuStore = defineStore({
|
||||
id: 'menu',
|
||||
state: () => ({
|
||||
menus: {} as {[key: string]: string},
|
||||
}),
|
||||
getters: {
|
||||
hasPermission(state) {
|
||||
return (menuCode: string | string[]) => {
|
||||
if (!menuCode) {
|
||||
return true
|
||||
}
|
||||
if (!!Object.keys(state.menus).length) {
|
||||
if (typeof menuCode === 'string') {
|
||||
return !!this.menus[menuCode]
|
||||
}
|
||||
return menuCode.some(code => !!this.menus[code])
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -37,7 +37,7 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<!-- <template #actions="item">
|
||||
<template #actions="item">
|
||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||
<a-button :disabled="item.disabled">
|
||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||
|
@ -56,7 +56,7 @@
|
|||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</template> -->
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
<template #id="slotProps">
|
||||
|
@ -152,27 +152,23 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
|||
tooltip: {
|
||||
title: '导入'
|
||||
},
|
||||
disabled: true,
|
||||
icon: 'icon-xiazai'
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
// disabled: true,
|
||||
text: "删除",
|
||||
disabled: !!data?.state,
|
||||
tooltip: {
|
||||
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
||||
},
|
||||
// popConfirm: {
|
||||
// title: '确认删除?'
|
||||
// },
|
||||
|
||||
popConfirm: {
|
||||
title: '确认删除?'
|
||||
},
|
||||
icon: 'icon-huishouzhan'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const p = h('p', 'hi')
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<a-drawer :mask-closable="false" title="查看物模型" width="700" v-model:visible="_visible" destroy-on-close @close="close">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleExport">
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="cat-content">
|
||||
<p class="cat-tip">
|
||||
物模型是对设备在云端的功能描述,包括设备的属性、服务和事件。物联网平台通过定义一种物的描述语言来描述物模型,称之为
|
||||
TSL(即 Thing Specification Language),采用 JSON 格式,您可以根据 TSL
|
||||
组装上报设备的数据。您可以导出完整物模型,用于云端应用开发。
|
||||
</p>
|
||||
</div>
|
||||
<a-tabs @change="handleConvertMetadata">
|
||||
<a-tab-pane v-for="item in codecs" :tab-key="item.id" :key="item.id">
|
||||
<div class="cat-panel">
|
||||
<!-- TODO 代码编辑器 -->
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script setup lang="ts" name="Cat">
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { downloadObject } from '@/utils/utils'
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import { useProductStore } from '@/store/product';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { convertMetadata, getCodecs, detail as productDetail } from '@/api/device/product';
|
||||
import { detail } from '@/api/device/instance'
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
type: 'product' | 'device';
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:visible', data: boolean): void;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits<Emits>()
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
|
||||
const _visible = computed({
|
||||
get: () => {
|
||||
return props.visible;
|
||||
},
|
||||
set: (val: any) => {
|
||||
emits('update:visible', val);
|
||||
},
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emits('update:visible', false);
|
||||
}
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
const productStore = useProductStore()
|
||||
const metadataMap = {
|
||||
product: productStore.current?.metadata as string,
|
||||
device: instanceStore.current?.metadata as string,
|
||||
};
|
||||
const metadata = metadataMap[props.type];
|
||||
const value = ref(metadata)
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
downloadObject(
|
||||
JSON.parse(value.value),
|
||||
`${props.type === 'device'
|
||||
? instanceStore.current?.name
|
||||
: productStore.current?.name
|
||||
}-物模型`,
|
||||
'YYYY/MM/DD',
|
||||
);
|
||||
} catch (e) {
|
||||
message.error('请先配置物模型');
|
||||
}
|
||||
}
|
||||
|
||||
const handleConvertMetadata = (key: Key) => {
|
||||
if (key === 'alink') {
|
||||
value.value = '';
|
||||
if (metadata) {
|
||||
convertMetadata('to', 'alink', JSON.parse(metadata)).then(res => {
|
||||
if (res.status === 200) {
|
||||
value.value = JSON.stringify(res.result)
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
value.value = metadata;
|
||||
}
|
||||
};
|
||||
|
||||
const codecs = ref<{ id: string; name: string }[]>()
|
||||
|
||||
const routeChange = async (id: string) => {
|
||||
const res = await getCodecs()
|
||||
if (res.status === 200) {
|
||||
codecs.value = [{ id: 'jetlinks', name: 'jetlinks' }].concat(res.result)
|
||||
}
|
||||
if (props.type === 'device' && id) {
|
||||
detail(id as string).then((resp) => {
|
||||
if (resp.status === 200) {
|
||||
instanceStore.setCurrent(resp.result);
|
||||
const _metadata = resp.result?.metadata;
|
||||
value.value = _metadata;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => route.params.id,
|
||||
(id) => routeChange(id as string),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.visible) {
|
||||
loading.value = true
|
||||
const { id } = route.params
|
||||
if (props.type === 'device') {
|
||||
detail(id as string).then((resp) => {
|
||||
loading.value = false
|
||||
instanceStore.setCurrent(resp.result)
|
||||
value.value = resp.result.metadata
|
||||
});
|
||||
} else {
|
||||
productDetail(id as string).then((resp) => {
|
||||
loading.value = false
|
||||
// productStore.setCurrent(resp.result)
|
||||
value.value = resp.result.metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.cat-content {
|
||||
background: #F6F6F6;
|
||||
|
||||
.cat-tip {
|
||||
padding: 10px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
.cat-panel {
|
||||
border: 1px solid #eeeeee;
|
||||
height: 670px;
|
||||
width: 650px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<a-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
|
||||
@ok="handleImport" :confirm-loading="loading">
|
||||
<div class="import-content">
|
||||
<p class="import-tip">
|
||||
<exclamation-circle-outlined style="margin-right: 5px" />
|
||||
导入的物模型会覆盖原来的属性、功能、事件、标签,请谨慎操作。
|
||||
</p>
|
||||
</div>
|
||||
<a-form layout="vertical" v-model="formModel">
|
||||
<a-form-item label="导入方式" v-bind="validateInfos.type">
|
||||
<a-select v-if="type === 'product'" v-model:value="formModel.type">
|
||||
<a-select-option value="copy">拷贝产品</a-select-option>
|
||||
<a-select-option value="import">导入物模型</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
|
||||
<a-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="物模型类型" v-bind="validateInfos.metadata"
|
||||
v-if="type === 'device' || formModel.type === 'import'">
|
||||
<a-select v-model:value="formModel.metadata">
|
||||
<a-select-option value="jetlinks">Jetlinks物模型</a-select-option>
|
||||
<a-select-option value="alink">阿里云物模型TSL</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="导入类型" v-bind="validateInfos.metadataType"
|
||||
v-if="type === 'device' || formModel.type === 'import'">
|
||||
<a-select v-model:value="formModel.metadataType">
|
||||
<a-select-option value="file">文件上传</a-select-option>
|
||||
<a-select-option value="script">脚本</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
|
||||
<a-upload v-model:file-list="formModel.upload" name="files" :before-upload="beforeUpload" accept=".json"
|
||||
:show-upload-list="false"></a-upload>
|
||||
</a-form-item>
|
||||
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
|
||||
<!-- TODO代码编辑器 -->
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup lang="ts" name="Import">
|
||||
import { useForm } from 'ant-design-vue/es/form';
|
||||
import { saveMetadata } from '@/api/device/instance'
|
||||
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
|
||||
import type { DefaultOptionType } from 'ant-design-vue/es/select';
|
||||
import { UploadProps } from 'ant-design-vue/es';
|
||||
import type { DeviceMetadata } from '@/views/device/Product/typings'
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import { SystemConst } from '@/utils/consts';
|
||||
import { useInstanceStore } from '@/store/instance'
|
||||
|
||||
const route = useRoute()
|
||||
const instanceStore = useInstanceStore()
|
||||
|
||||
interface Props {
|
||||
visible: boolean,
|
||||
type: 'device' | 'product',
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:visible', data: boolean): void;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits<Emits>()
|
||||
const loading = ref(false)
|
||||
|
||||
const _visible = computed({
|
||||
get: () => {
|
||||
return props.visible;
|
||||
},
|
||||
set: (val: any) => {
|
||||
emits('update:visible', val);
|
||||
},
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emits('update:visible', false);
|
||||
}
|
||||
|
||||
/** form表单 */
|
||||
const formModel = reactive<Record<string, any>>({
|
||||
type: 'import',
|
||||
metadata: 'jetlinks',
|
||||
metadataType: 'script',
|
||||
})
|
||||
const rules = reactive({
|
||||
type: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择导入方式',
|
||||
},
|
||||
],
|
||||
copy: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择产品',
|
||||
},
|
||||
],
|
||||
metadata: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择物模型类型',
|
||||
},
|
||||
],
|
||||
metadataType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择导入类型',
|
||||
},
|
||||
],
|
||||
upload: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传文件',
|
||||
},
|
||||
],
|
||||
import: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入物模型',
|
||||
},
|
||||
],
|
||||
})
|
||||
const { validate, validateInfos } = useForm(formModel, rules);
|
||||
const onSubmit = () => {
|
||||
validate().then(() => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
const productList = ref<DefaultOptionType[]>([])
|
||||
|
||||
const loadData = async () => {
|
||||
const { id } = route.params || {}
|
||||
const product = await queryNoPagingPost({
|
||||
paging: false,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [{ column: 'id$not', value: id }],
|
||||
}) as any
|
||||
productList.value = product.result.filter((i: any) => i?.metadata).map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.metadata,
|
||||
key: item.id
|
||||
})) as DefaultOptionType[]
|
||||
}
|
||||
loadData()
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = file => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = (json) => {
|
||||
formModel.import = json.target?.result;
|
||||
};
|
||||
}
|
||||
|
||||
const operateLimits = (mdata: DeviceMetadata) => {
|
||||
const obj: DeviceMetadata = { ...mdata };
|
||||
const old = JSON.parse(instanceStore.detail?.metadata || '{}');
|
||||
const fid = instanceStore.detail?.features?.map(item => item.id);
|
||||
if (fid?.includes('eventNotModifiable')) {
|
||||
obj.events = old?.events || [];
|
||||
}
|
||||
if (fid?.includes('propertyNotModifiable')) {
|
||||
obj.properties = old?.properties || [];
|
||||
}
|
||||
(obj?.events || []).map((item, index) => {
|
||||
return { ...item, sortsIndex: index };
|
||||
});
|
||||
(obj?.properties || []).map((item, index) => {
|
||||
return { ...item, sortsIndex: index };
|
||||
});
|
||||
(obj?.functions || []).map((item, index) => {
|
||||
return { ...item, sortsIndex: index };
|
||||
});
|
||||
(obj?.tags || []).map((item, index) => {
|
||||
return { ...item, sortsIndex: index };
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
validate().then(async (data) => {
|
||||
loading.value = true
|
||||
if (data.metadata === 'alink') {
|
||||
const res = await convertMetadata('from', 'alink', data.import)
|
||||
if (res.status === 200) {
|
||||
const metadata = JSON.stringify(operateLimits(res.result))
|
||||
const { id } = route.params || {}
|
||||
if (props?.type === 'device') {
|
||||
await saveMetadata(id as string, metadata)
|
||||
} else {
|
||||
await modify(id as string, { metadata: metadata })
|
||||
}
|
||||
loading.value = false
|
||||
// MetadataAction.insert(JSON.parse(metadata || '{}'));
|
||||
message.success('导入成功')
|
||||
} else {
|
||||
loading.value = false
|
||||
message.error('发生错误!')
|
||||
}
|
||||
Store.set(SystemConst.GET_METADATA, true)
|
||||
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||
close()
|
||||
} else {
|
||||
try {
|
||||
const _object = JSON.parse(data[props?.type === 'device' ? 'import' : data?.type] || '{}')
|
||||
if (
|
||||
!(!!_object?.properties || !!_object?.events || !!_object?.functions || !!_object?.tags)
|
||||
) {
|
||||
message.error('物模型数据不正确')
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
const { id } = route.params || {}
|
||||
const params = {
|
||||
id,
|
||||
metadata: JSON.stringify(operateLimits(_object as DeviceMetadata)),
|
||||
};
|
||||
const paramsDevice = JSON.stringify(operateLimits(_object as DeviceMetadata))
|
||||
let resp = undefined
|
||||
if (props?.type === 'device') {
|
||||
resp = await saveMetadata(id as string, paramsDevice)
|
||||
} else {
|
||||
resp = await modify(id as string, params)
|
||||
}
|
||||
loading.value = false
|
||||
if (resp.status === 200) {
|
||||
if (props?.type === 'device') {
|
||||
const metadata: DeviceMetadata = JSON.parse(paramsDevice || '{}')
|
||||
// MetadataAction.insert(metadata);
|
||||
message.success('导入成功')
|
||||
} else {
|
||||
const metadata: DeviceMetadata = JSON.parse(params?.metadata || '{}')
|
||||
// MetadataAction.insert(metadata);
|
||||
message.success('导入成功')
|
||||
}
|
||||
}
|
||||
Store.set(SystemConst.GET_METADATA, true)
|
||||
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||
close();
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
message.error(e === 'error' ? '物模型数据不正确' : '上传json格式的物模型文件')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// const showProduct = computed(() => formModel.type === 'copy')
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.import-content {
|
||||
background: rgb(236, 237, 238);
|
||||
|
||||
.import-tip {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div class='device-detail-metadata' style="position: relative;">
|
||||
<div class="tips" style="width: 40%">
|
||||
<a-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device'
|
||||
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
|
||||
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
|
||||
<div class="ellipsis">
|
||||
<info-circle-outlined style="margin-right: 3px" />
|
||||
{{
|
||||
instanceStore.detail?.independentMetadata && type === 'device'
|
||||
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
|
||||
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'
|
||||
}}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-tabs class="metadataNav" destroyInactiveTabPane>
|
||||
<template #rightExtra>
|
||||
<a-space>
|
||||
<PermissionButton v-if="type === 'device'" :hasPermission="`${permission}:update`"
|
||||
:popConfirm="{ title: '确认重置?', onConfirm: resetMetadata, }" :tooltip="{ title: '重置后将使用产品的物模型配置' }"
|
||||
key="reload">
|
||||
重置操作
|
||||
</PermissionButton>
|
||||
<PermissionButton :isPermission="`${permission}:update`" @click="visible = true">快速导入</PermissionButton>
|
||||
<PermissionButton :isPermission="`${permission}:update`" @click="cat = true">物模型TSL</PermissionButton>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-tab-pane tab="属性定义" key="properties">
|
||||
<BaseMetadata target={props.type} type="properties" :permission="permission" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="功能定义" key="functions">
|
||||
<BaseMetadata target={props.type} type="functions" :permission="permission" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="事件定义" key="events">
|
||||
<BaseMetadata target={props.type} type="events" :permission="permission" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="标签定义" key="tags">
|
||||
<BaseMetadata target={props.type} type="tags" :permission="permission" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<Import :visible="visible" :type="type" @close="visible = false" />
|
||||
<Cat :visible="cat" @close="cat = false" :type="type" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Metadata">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue'
|
||||
import { deleteMetadata } from '@/api/device/instance.js'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Store } from 'jetlinks-store'
|
||||
import { SystemConst } from '@/utils/consts'
|
||||
import { useInstanceStore } from '@/store/instance'
|
||||
import Import from './Import/index.vue'
|
||||
import Cat from './Cat/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const instanceStore = useInstanceStore()
|
||||
interface Props {
|
||||
type: 'product' | 'device';
|
||||
independentMetadata?: boolean;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const permission = computed(() => props.type === 'device' ? 'device/Instance' : 'device/Product')
|
||||
const visible = ref(false)
|
||||
const cat = ref(false)
|
||||
|
||||
// 重置物模型
|
||||
const resetMetadata = async () => {
|
||||
const { id } = route.params
|
||||
const resp = await deleteMetadata(id as string)
|
||||
if (resp.status === 200) {
|
||||
message.info('操作成功')
|
||||
|
||||
Store.set(SystemConst.REFRESH_DEVICE, true)
|
||||
setTimeout(() => {
|
||||
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.device-detail-metadata {
|
||||
.tips {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
z-index: 1;
|
||||
margin-left: 330px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.metadataNav {
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,65 +1,63 @@
|
|||
<!-- webhook请求头可编辑表格 -->
|
||||
<template>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
bordered
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="['KEY', 'VALUE'].includes(column.dataIndex)">
|
||||
<a-input v-model="record[column.dataIndex]" />
|
||||
<div class="table-wrapper">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
bordered
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="['key', 'value'].includes(column.dataIndex)">
|
||||
<a-input v-model:value="record[column.dataIndex]" />
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'">
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<delete-outlined @click="handleDelete(record.id)" />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'">
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<delete-outlined @click="handleDelete(record.idx)" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-table>
|
||||
<a-button
|
||||
type="dashed"
|
||||
@click="handleAdd"
|
||||
style="width: 100%; margin-top: 5px"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button
|
||||
type="dashed"
|
||||
@click="handleAdd"
|
||||
style="width: 100%; margin-top: 5px"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
添加
|
||||
</a-button>
|
||||
添加
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
// import { cloneDeep } from 'lodash-es';
|
||||
// import { defineComponent, reactive, ref } from 'vue';
|
||||
// import type { UnwrapRef } from 'vue';
|
||||
import { PropType } from 'vue';
|
||||
import { IHeaders } from '../../types';
|
||||
|
||||
interface DataItem {
|
||||
idx: number;
|
||||
KEY: string;
|
||||
VALUE: string;
|
||||
}
|
||||
type Emits = {
|
||||
(e: 'update:headers', data: IHeaders[]): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const data: DataItem[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
data.push({
|
||||
idx: i,
|
||||
KEY: `key ${i}`,
|
||||
VALUE: `value${i}`,
|
||||
});
|
||||
}
|
||||
const props = defineProps({
|
||||
headers: {
|
||||
type: Array as PropType<IHeaders[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'KEY',
|
||||
dataIndex: 'KEY',
|
||||
dataIndex: 'key',
|
||||
},
|
||||
{
|
||||
title: 'VALUE',
|
||||
dataIndex: 'VALUE',
|
||||
dataIndex: 'value',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -69,17 +67,20 @@ const columns = [
|
|||
},
|
||||
];
|
||||
|
||||
const dataSource = ref(data);
|
||||
console.log('dataSource: ', dataSource.value);
|
||||
const dataSource = computed({
|
||||
get: () => props.headers,
|
||||
set: (val) => emit('update:headers', val),
|
||||
});
|
||||
|
||||
const handleDelete = (idx: number) => {
|
||||
const handleDelete = (id: number) => {
|
||||
const idx = dataSource.value.findIndex((f) => f.id === id);
|
||||
dataSource.value.splice(idx, 1);
|
||||
};
|
||||
const handleAdd = () => {
|
||||
dataSource.value.push({
|
||||
idx: dataSource.value.length + 1,
|
||||
KEY: `key ${dataSource.value.length + 1}`,
|
||||
VALUE: `value ${dataSource.value.length + 1}`,
|
||||
id: dataSource.value.length,
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -259,6 +259,7 @@
|
|||
<a-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="btnLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
保存
|
||||
|
@ -283,8 +284,12 @@ import {
|
|||
MSG_TYPE,
|
||||
} from '@/views/notice/const';
|
||||
import regionList from './regionId';
|
||||
import EditTable from './components/EditTable.vue'
|
||||
import EditTable from './components/EditTable.vue';
|
||||
|
||||
import configApi from '@/api/notice/config';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const useForm = Form.useForm;
|
||||
|
||||
// 消息类型
|
||||
|
@ -311,9 +316,8 @@ const formData = ref<ConfigFormData>({
|
|||
description: '',
|
||||
name: '',
|
||||
provider: 'dingTalkMessage',
|
||||
type: NOTICE_METHOD[0].value,
|
||||
type: 'dingTalk',
|
||||
});
|
||||
|
||||
// 根据通知方式展示对应的字段
|
||||
watch(
|
||||
() => formData.value.type,
|
||||
|
@ -383,25 +387,56 @@ const formRules = ref({
|
|||
pattern:
|
||||
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
|
||||
message: 'Webhook需要是一个合法的URL',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
});
|
||||
|
||||
const { resetFields, validate, validateInfos } = useForm(
|
||||
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||
formData.value,
|
||||
formRules.value,
|
||||
);
|
||||
console.log('validateInfos: ', validateInfos);
|
||||
watch(
|
||||
() => formData.value.type,
|
||||
() => {
|
||||
clearValidate();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const getDetail = async () => {
|
||||
const res = await configApi.detail(route.params.id as string);
|
||||
console.log('res: ', res);
|
||||
formData.value = res.result;
|
||||
console.log('formData.value: ', formData.value);
|
||||
};
|
||||
getDetail();
|
||||
|
||||
/**
|
||||
* 表单提交
|
||||
*/
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const handleSubmit = () => {
|
||||
validate()
|
||||
.then(async () => {})
|
||||
.catch((err) => {});
|
||||
.then(async () => {
|
||||
// console.log('formData.value: ', formData.value);
|
||||
btnLoading.value = true;
|
||||
let res;
|
||||
if (!formData.value.id) {
|
||||
res = await configApi.save(formData.value);
|
||||
} else {
|
||||
res = await configApi.update(formData.value);
|
||||
}
|
||||
// console.log('res: ', res);
|
||||
if (res?.success) {
|
||||
message.success('保存成功');
|
||||
router.back();
|
||||
}
|
||||
btnLoading.value = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err: ', err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -3,6 +3,20 @@
|
|||
<div class="page-container">通知配置</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import configApi from '@/api/notice/config';
|
||||
|
||||
const getList = async () => {
|
||||
const res = await configApi.list({
|
||||
current: 1,
|
||||
pageIndex: 0,
|
||||
pageSize: 12,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [],
|
||||
});
|
||||
console.log('res: ', res);
|
||||
};
|
||||
getList();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
interface IHeaders {
|
||||
export interface IHeaders {
|
||||
id?: number;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
@ -34,4 +35,8 @@ export type ConfigFormData = {
|
|||
name: string;
|
||||
provider: string;
|
||||
type: string;
|
||||
id?: string;
|
||||
maxRetryTimes?: number;
|
||||
creatorId?: string;
|
||||
createTime?: number;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue