Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
6d01346d51
|
@ -15,6 +15,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
"@vuemap/vue-amap": "^1.1.20",
|
"@vuemap/vue-amap": "^1.1.20",
|
||||||
|
"@vueuse/core": "^9.10.0",
|
||||||
"ant-design-vue": "^3.2.15",
|
"ant-design-vue": "^3.2.15",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"echarts": "^5.4.1",
|
"echarts": "^5.4.1",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import server from '@/utils/request'
|
import server from '@/utils/request'
|
||||||
|
import { DeviceInstance } from '@/views/device/instance/typings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除设备物模型
|
* 删除设备物模型
|
||||||
|
@ -13,4 +14,11 @@ export const deleteMetadata = (deviceId: string) => server.remove(`/device-insta
|
||||||
* @param data 物模型
|
* @param data 物模型
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const saveMetadata = (id: string, data: string) => server.put(`/device/instance/${id}/metadata`, data)
|
export const saveMetadata = (id: string, data: string) => server.put(`/device/instance/${id}/metadata`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备ID获取设备详情
|
||||||
|
* @param id 设备ID
|
||||||
|
* @returns 设备详情
|
||||||
|
*/
|
||||||
|
export const detail = (id: string) => server.get<DeviceInstance>(`/device-instance/${id}/detail`)
|
|
@ -1,5 +1,5 @@
|
||||||
import server from '@/utils/request'
|
import server from '@/utils/request'
|
||||||
import type { DeviceMetadata } from '@/views/device/Product/typings'
|
import { DeviceMetadata, ProductItem } from '@/views/device/Product/typings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条件查询产品(不带翻页)
|
* 根据条件查询产品(不带翻页)
|
||||||
|
@ -23,4 +23,17 @@ export const convertMetadata = (direction: 'from' | 'to', type: string, data: an
|
||||||
* @param data 产品数据
|
* @param data 产品数据
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const modify = (id: string, data: any) => server.put(`/device-product/${id}`, data)
|
export const modify = (id: string, data: any) => server.put(`/device-product/${id}`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getCodecs = () => server.get<{id: string, name: string}>('/device/product/metadata/codecs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据产品ID获取产品详情
|
||||||
|
* @param id 产品ID
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`)
|
|
@ -5,4 +5,4 @@ export const getDeviceCount_api = () => server.get(`/device/instance/_count`);
|
||||||
// 产品数量
|
// 产品数量
|
||||||
export const getProductCount_api = (data) => server.post(`/device-product/_count`, data);
|
export const getProductCount_api = (data) => server.post(`/device-product/_count`, data);
|
||||||
// 查询产品列表
|
// 查询产品列表
|
||||||
export const getProductList_api = (data) => server.get(`/device/product/_query/no-paging?paging=false`, data);
|
export const getProductList_api = (data) => server.get(`/device/product/_query/no-paging?paging=false`, data);
|
||||||
|
|
|
@ -12,4 +12,6 @@ export const postInitSet = (data) => server.post(`/user/settings/init`, data)
|
||||||
|
|
||||||
export const systemVersion = () => server.get(`/system/version`)
|
export const systemVersion = () => server.get(`/system/version`)
|
||||||
|
|
||||||
export const bindInfo = () => server.get(`/application/sso/_all`)
|
export const bindInfo = () => server.get(`/application/sso/_all`)
|
||||||
|
|
||||||
|
export const settingDetail = (scopes) => server.get(`/system/config/${scopes}`)
|
|
@ -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',
|
delete: item.key === 'delete',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- <slot name="actions" v-bind="item"></slot> -->
|
<slot name="actions" v-bind="item"></slot>
|
||||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||||
<a-button :disabled="item.disabled">
|
<a-button :disabled="item.disabled">
|
||||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
@ -284,13 +284,14 @@ const handleClick = () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
& > span,
|
& > :deep(span, button) {
|
||||||
button {
|
width: 100%;
|
||||||
width: 100% !important;
|
border-radius: 0;
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
:deep(button) {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
color: #2f54eb;
|
color: #2f54eb;
|
||||||
|
@ -322,7 +323,7 @@ const handleClick = () => {
|
||||||
flex-basis: 60px;
|
flex-basis: 60px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
||||||
button {
|
:deep(button) {
|
||||||
background: @error-color-deprecated-bg;
|
background: @error-color-deprecated-bg;
|
||||||
border: 1px solid @error-color-outline;
|
border: 1px solid @error-color-outline;
|
||||||
|
|
||||||
|
@ -348,7 +349,7 @@ const handleClick = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] {
|
:deep(button[disabled]) {
|
||||||
background: @disabled-bg;
|
background: @disabled-bg;
|
||||||
border-color: @disabled-color;
|
border-color: @disabled-color;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='JSearch-item'>
|
<div class='JSearch-item'>
|
||||||
<div class='JSearch-item--type'>
|
<div class='JSearch-item--type' v-if='expand'>
|
||||||
<a-select
|
<a-select
|
||||||
v-if='index !== 1 && index !== 4'
|
v-if='index !== 1 && index !== 4'
|
||||||
:options='typeOptions'
|
:options='typeOptions'
|
||||||
|
@ -27,45 +27,55 @@
|
||||||
<a-input
|
<a-input
|
||||||
v-if='component === componentType.input'
|
v-if='component === componentType.input'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
v-else-if='component === componentType.select'
|
v-else-if='component === componentType.select'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:options='options'
|
:options='options'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-inputnumber
|
<a-inputnumber
|
||||||
v-else-if='component === componentType.inputNumber'
|
v-else-if='component === componentType.inputNumber'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-input-password
|
<a-input-password
|
||||||
v-else-if='component === componentType.password'
|
v-else-if='component === componentType.password'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-switch
|
<a-switch
|
||||||
v-else-if='component === componentType.switch'
|
v-else-if='component === componentType.switch'
|
||||||
v-model:checked='termsModel.value'
|
v-model:checked='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-radio-group
|
<a-radio-group
|
||||||
v-else-if='component === componentType.radio'
|
v-else-if='component === componentType.radio'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-checkbox-group
|
<a-checkbox-group
|
||||||
v-else-if='component === componentType.checkbox'
|
v-else-if='component === componentType.checkbox'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:options='options'
|
:options='options'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-time-picker
|
<a-time-picker
|
||||||
v-else-if='component === componentType.time'
|
v-else-if='component === componentType.time'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
v-else-if='component === componentType.date'
|
v-else-if='component === componentType.date'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
<a-tree-select
|
<a-tree-select
|
||||||
v-else-if='component === componentType.tree'
|
v-else-if='component === componentType.tree'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:tree-data='options'
|
:tree-data='options'
|
||||||
|
@change='(v) => valueChange(v)'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,27 +83,99 @@
|
||||||
|
|
||||||
<script setup lang='ts' name='SearchItem'>
|
<script setup lang='ts' name='SearchItem'>
|
||||||
import { componentType } from 'components/Form'
|
import { componentType } from 'components/Form'
|
||||||
import { typeOptions } from './util'
|
import { typeOptions, termType } from './util'
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import type { SearchItemProps, SearchItemData } from './types'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
|
type ItemDataProps = Omit<SearchItemData, 'title'>
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'change', data: ItemDataProps): void
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
columns: {
|
||||||
type: String,
|
type: Array as PropType<SearchItemProps[]>,
|
||||||
default: componentType.input
|
default: () => [],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
index: {
|
index: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const termsModel = reactive({})
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const termsModel = reactive<ItemDataProps>({
|
||||||
|
type: 'or',
|
||||||
|
value: '',
|
||||||
|
termType: 'eq',
|
||||||
|
column: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const component = ref(componentType.input)
|
||||||
|
|
||||||
const options = ref([])
|
const options = ref([])
|
||||||
|
|
||||||
const columnOptions = reactive([])
|
const columnOptions = ref<({ label: string, value: string})[]>([])
|
||||||
|
const columnOptionMap = new Map()
|
||||||
|
|
||||||
const termTypeOptions = reactive([])
|
const termTypeOptions = reactive(termType)
|
||||||
|
|
||||||
|
const getTermType = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'select':
|
||||||
|
case 'treeSelect':
|
||||||
|
return 'eq'
|
||||||
|
case 'date':
|
||||||
|
case 'time':
|
||||||
|
return 'gt'
|
||||||
|
default:
|
||||||
|
return 'like'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleItem = () => {
|
||||||
|
columnOptionMap.clear()
|
||||||
|
columnOptions.value = []
|
||||||
|
if (!props.columns.length) return
|
||||||
|
|
||||||
|
// 获取第一个值
|
||||||
|
|
||||||
|
const sortColumn = cloneDeep(props.columns)
|
||||||
|
sortColumn?.sort((a, b) => a.sortIndex! - b.sortIndex!)
|
||||||
|
|
||||||
|
const _index = props.index > sortColumn.length ? sortColumn.length - 1 : props.index
|
||||||
|
const _itemColumn = sortColumn[_index - 1]
|
||||||
|
|
||||||
|
termsModel.column = _itemColumn.column
|
||||||
|
termsModel.termType = _itemColumn.defaultTermType || getTermType(_itemColumn.type as string)
|
||||||
|
|
||||||
|
columnOptions.value = props.columns.map(item => {
|
||||||
|
columnOptionMap.set(item.column, item)
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.column
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueChange = (value: any) => {
|
||||||
|
emit('change', {
|
||||||
|
type: termsModel.type,
|
||||||
|
value: termsModel.value,
|
||||||
|
termType: termsModel.termType,
|
||||||
|
column: termsModel.column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItem()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -103,7 +185,7 @@ const termTypeOptions = reactive([])
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.JSearch-item--type {
|
.JSearch-item--type {
|
||||||
min-width: 120px;
|
min-width: 80px;
|
||||||
> span {
|
> span {
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -111,11 +193,11 @@ const termTypeOptions = reactive([])
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--column {
|
.JSearch-item--column {
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--termType {
|
.JSearch-item--termType {
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--value {
|
.JSearch-item--value {
|
||||||
|
|
|
@ -1,19 +1,74 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='JSearch-content'>
|
<div class='JSearch-warp' ref='searchRef'>
|
||||||
<div class='left'>
|
<!-- 高级模式 -->
|
||||||
<SearchItem :index='1' />
|
<div v-if='props.type === "advanced"' :class='["JSearch-content senior", expand ? "senior-expand" : "", screenSize ? "big" : "small"]'>
|
||||||
<SearchItem :index='2' />
|
<div :class='["JSearch-items", expand ? "items-expand" : "", layout]'>
|
||||||
<SearchItem :index='3' />
|
<div class='left'>
|
||||||
|
<SearchItem :expand='expand' :index='1' :columns='searchItems' />
|
||||||
|
<SearchItem v-if='expand' :expand='expand' :index='2' :columns='searchItems' />
|
||||||
|
<SearchItem v-if='expand' :expand='expand' :index='3' :columns='searchItems' />
|
||||||
|
</div>
|
||||||
|
<div class='center' v-if='expand'>
|
||||||
|
<a-select
|
||||||
|
v-model:value='termType'
|
||||||
|
class='center-select'
|
||||||
|
:options='typeOptions'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class='right' v-if='expand'>
|
||||||
|
<SearchItem :expand='expand' :index='4' :columns='searchItems' />
|
||||||
|
<SearchItem :expand='expand' :index='5' :columns='searchItems' />
|
||||||
|
<SearchItem :expand='expand' :index='6' :columns='searchItems' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class='["JSearch-footer", expand ? "expand" : ""]'>
|
||||||
|
<div class='JSearch-footer--btns'>
|
||||||
|
<a-dropdown-button type="primary">
|
||||||
|
搜索
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu v-if='!!historyList.length'>
|
||||||
|
<a-menu-item>
|
||||||
|
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-empty v-else />
|
||||||
|
</template>
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
</a-dropdown-button>
|
||||||
|
<a-button>
|
||||||
|
<template #icon><PoweroffOutlined /></template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
<a-button>
|
||||||
|
<template #icon><PoweroffOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-button type='link' class='more-btn' @click='expandChange'>
|
||||||
|
更多筛选
|
||||||
|
<DownOutlined :class='["more-icon",expand ? "more-up" : "more-down"]' />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='center'>
|
<!-- 简单模式 -->
|
||||||
<a-select
|
<div v-else class='JSearch-content simple big'>
|
||||||
:options='typeOptions'
|
<div class='JSearch-items'>
|
||||||
/>
|
<div class='left'>
|
||||||
</div>
|
<SearchItem :expand='false' :index='1' />
|
||||||
<div class='right'>
|
</div>
|
||||||
<SearchItem :index='4' />
|
</div>
|
||||||
<SearchItem :index='5' />
|
<div class='JSearch-footer'>
|
||||||
<SearchItem :index='6' />
|
<div class='JSearch-footer--btns'>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
搜索
|
||||||
|
</a-button>
|
||||||
|
<a-button>
|
||||||
|
<template #icon><PoweroffOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,6 +76,12 @@
|
||||||
<script setup lang='ts' name='Search'>
|
<script setup lang='ts' name='Search'>
|
||||||
import SearchItem from './Item.vue'
|
import SearchItem from './Item.vue'
|
||||||
import { typeOptions } from './util'
|
import { typeOptions } from './util'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
import { omit } from 'lodash-es'
|
||||||
|
import { SearchOutlined, DownOutlined } from '@ant-design/icons-vue';
|
||||||
|
import type { SearchItemProps } from './types'
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { JColumnsProps } from 'components/Table/types'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultParams: {
|
defaultParams: {
|
||||||
|
@ -28,14 +89,14 @@ const props = defineProps({
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array as PropType<JColumnsProps[]>,
|
||||||
default: () => []
|
default: () => [],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'advanced'
|
default: 'advanced'
|
||||||
},
|
},
|
||||||
|
|
||||||
key: {
|
key: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -43,30 +104,153 @@ const props = defineProps({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const searchRef = ref(null)
|
||||||
|
const { width } = useElementSize(searchRef)
|
||||||
|
|
||||||
|
// 是否展开更多筛选
|
||||||
|
const expand = ref(false)
|
||||||
|
|
||||||
|
// 第一组,第二组的关系
|
||||||
|
const termType = ref('or')
|
||||||
|
// 搜索历史记录
|
||||||
|
const historyList = ref([])
|
||||||
|
|
||||||
|
// 组件方向
|
||||||
|
const layout = ref('horizontal')
|
||||||
|
// 当前组件宽度 true 大于1000
|
||||||
|
const screenSize = ref(true)
|
||||||
|
|
||||||
|
const searchItems = ref<SearchItemProps[]>([])
|
||||||
|
|
||||||
|
const expandChange = () => {
|
||||||
|
expand.value = !expand.value
|
||||||
|
}
|
||||||
|
|
||||||
const searchParams = reactive({
|
const searchParams = reactive({
|
||||||
data: {}
|
data: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleItems = () => {
|
||||||
|
searchItems.value = []
|
||||||
|
props.columns!.forEach((item, index) => {
|
||||||
|
if (item.search && Object.keys(item.search).length) {
|
||||||
|
searchItems.value.push({
|
||||||
|
...omit(item.search, ['rename']),
|
||||||
|
sortIndex: item.search.first ? 0 : index + 1,
|
||||||
|
title: item.title,
|
||||||
|
column: item.dataIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(width, (value) => {
|
||||||
|
if (value < 1000) {
|
||||||
|
layout.value = 'vertical'
|
||||||
|
screenSize.value = false
|
||||||
|
} else {
|
||||||
|
layout.value = 'horizontal'
|
||||||
|
screenSize.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handleItems()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang='less'>
|
<style scoped lang='less'>
|
||||||
.JSearch-content {
|
.JSearch-warp {
|
||||||
display: flex;
|
padding: 24px;
|
||||||
gap: 16px;
|
background-color: #fff;
|
||||||
.left, & .right {
|
|
||||||
|
.JSearch-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
flex-direction: column;
|
|
||||||
width: 0;
|
.JSearch-items,& .JSearch-footer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 0;
|
}
|
||||||
}
|
|
||||||
|
.JSearch-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.left, & .right {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
.left,& .right,& .center {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.center-select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.JSearch-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 64px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.expand {
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.center {
|
.more-btn {
|
||||||
display: flex;
|
position: absolute;
|
||||||
flex-direction: column;
|
right: 0;
|
||||||
justify-content: center;
|
}
|
||||||
flex-basis: 120px;
|
}
|
||||||
|
|
||||||
|
.JSearch-footer--btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.senior-expand {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-up {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.big {
|
||||||
|
gap: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.simple {
|
||||||
|
.JSearch-items {
|
||||||
|
flex-grow: 4;
|
||||||
|
}
|
||||||
|
.JSearch-footer {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
export interface SearchProps {
|
||||||
|
rename?: string
|
||||||
|
type?: 'select' | 'number' | 'string' | 'treeSelect' | 'date' | 'time'
|
||||||
|
format?: string
|
||||||
|
options?: any[] | Function
|
||||||
|
first?: boolean
|
||||||
|
defaultTermType?: string // 默认 eq
|
||||||
|
title?: ColumnType.title
|
||||||
|
sortIndex?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchItemData {
|
||||||
|
column: ColumnType.dataIndex
|
||||||
|
rename?: string
|
||||||
|
value: any
|
||||||
|
termType: string
|
||||||
|
type?: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchItemProps extends SearchProps, SearchItemData {}
|
|
@ -1,4 +1,17 @@
|
||||||
export const typeOptions = [
|
export const typeOptions = [
|
||||||
{ label: '或者', value: 'or' },
|
{ label: '或者', value: 'or' },
|
||||||
{ label: '并且', value: 'and' },
|
{ label: '并且', value: 'and' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const termType = [
|
||||||
|
{ label: '=', value: 'eq' },
|
||||||
|
{ label: '!=', value: 'not' },
|
||||||
|
{ label: '包含', value: 'like' },
|
||||||
|
{ label: '不包含', value: 'nlike' },
|
||||||
|
{ label: '>', value: 'gt' },
|
||||||
|
{ label: '>=', value: 'gte' },
|
||||||
|
{ label: '<', value: 'lt' },
|
||||||
|
{ label: '<=', value: 'lte' },
|
||||||
|
{ label: '属于', value: 'in' },
|
||||||
|
{ label: '不属于', value: 'nin' },
|
||||||
|
];
|
|
@ -11,16 +11,21 @@ enum ModelEnum {
|
||||||
CARD = 'CARD',
|
CARD = 'CARD',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TypeEnum {
|
||||||
|
TREE = 'TREE',
|
||||||
|
PAGE = 'PAGE',
|
||||||
|
}
|
||||||
|
|
||||||
type RequestData = {
|
type RequestData = {
|
||||||
code: string;
|
code: string;
|
||||||
result: {
|
result: {
|
||||||
data: Record<string, any>[] | undefined;
|
data?: Record<string, any>[] | undefined;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
status: number;
|
status: number;
|
||||||
} & Record<string, any>;
|
} | Record<string, any>;
|
||||||
|
|
||||||
export interface ActionsType {
|
export interface ActionsType {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -39,16 +44,10 @@ export interface JColumnProps extends ColumnProps{
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JTableProps extends TableProps{
|
export interface JTableProps extends TableProps{
|
||||||
request?: (params: Record<string, any> & {
|
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
|
||||||
pageSize: number;
|
|
||||||
pageIndex: number;
|
|
||||||
}) => Promise<Partial<RequestData>>;
|
|
||||||
cardBodyClass?: string;
|
cardBodyClass?: string;
|
||||||
columns: JColumnProps[];
|
columns: JColumnProps[];
|
||||||
params?: Record<string, any> & {
|
params?: Record<string, any>;
|
||||||
pageSize: number;
|
|
||||||
pageIndex: number;
|
|
||||||
};
|
|
||||||
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
||||||
// actions?: ActionsType[];
|
// actions?: ActionsType[];
|
||||||
noPagination?: boolean;
|
noPagination?: boolean;
|
||||||
|
@ -64,6 +63,8 @@ export interface JTableProps extends TableProps{
|
||||||
*/
|
*/
|
||||||
gridColumns?: number[];
|
gridColumns?: number[];
|
||||||
alertRender?: boolean;
|
alertRender?: boolean;
|
||||||
|
type?: keyof typeof TypeEnum;
|
||||||
|
defaultParams?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JTable = defineComponent<JTableProps>({
|
const JTable = defineComponent<JTableProps>({
|
||||||
|
@ -96,10 +97,6 @@ const JTable = defineComponent<JTableProps>({
|
||||||
type: [String, undefined],
|
type: [String, undefined],
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
// actions: {
|
|
||||||
// type: Array as PropType<ActionsType[]>,
|
|
||||||
// default: () => []
|
|
||||||
// },
|
|
||||||
noPagination: {
|
noPagination: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
@ -127,6 +124,19 @@ const JTable = defineComponent<JTableProps>({
|
||||||
alertRender: {
|
alertRender: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'PAGE'
|
||||||
|
},
|
||||||
|
defaultParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as any,
|
} as any,
|
||||||
setup(props: JTableProps ,{ slots, emit }){
|
setup(props: JTableProps ,{ slots, emit }){
|
||||||
|
@ -162,25 +172,30 @@ const JTable = defineComponent<JTableProps>({
|
||||||
const handleSearch = async (_params?: Record<string, any>) => {
|
const handleSearch = async (_params?: Record<string, any>) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if(props.request) {
|
if(props.request) {
|
||||||
const resp = await props.request({
|
const resp = await props.request({
|
||||||
pageSize: 12,
|
...props.defaultParams,
|
||||||
pageIndex: 1,
|
|
||||||
..._params
|
..._params
|
||||||
})
|
})
|
||||||
if(resp.status === 200){
|
if(resp.status === 200){
|
||||||
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
if(props.type === 'PAGE'){
|
||||||
if(resp.result?.data?.length === 0 && resp.result.total && resp.result.pageSize && resp.result.pageIndex) {
|
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
||||||
handleSearch({
|
if(resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
|
||||||
..._params,
|
handleSearch({
|
||||||
pageSize: pageSize.value,
|
..._params,
|
||||||
pageIndex: pageIndex.value - 1,
|
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 {
|
} else {
|
||||||
_dataSource.value = resp.result?.data || []
|
_dataSource.value = resp?.result || []
|
||||||
pageIndex.value = resp.result?.pageIndex || 0
|
|
||||||
pageSize.value = resp.result?.pageSize || 6
|
|
||||||
total.value = resp.result?.total || 0
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_dataSource.value = []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_dataSource.value = props?.dataSource || []
|
_dataSource.value = props?.dataSource || []
|
||||||
|
@ -282,7 +297,7 @@ const JTable = defineComponent<JTableProps>({
|
||||||
</div>
|
</div>
|
||||||
{/* 分页 */}
|
{/* 分页 */}
|
||||||
{
|
{
|
||||||
_dataSource.value.length && !props.noPagination &&
|
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
|
||||||
<div class={styles['jtable-pagination']}>
|
<div class={styles['jtable-pagination']}>
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -292,14 +307,16 @@ const JTable = defineComponent<JTableProps>({
|
||||||
current={pageIndex.value}
|
current={pageIndex.value}
|
||||||
pageSize={pageSize.value}
|
pageSize={pageSize.value}
|
||||||
pageSizeOptions={['12', '24', '48', '60', '100']}
|
pageSizeOptions={['12', '24', '48', '60', '100']}
|
||||||
showTotal={(total, range) => {
|
showTotal={(num) => {
|
||||||
return `第 ${range[0]} - ${range[1]} 条/总共 ${total} 条`
|
const minSize = pageIndex.value * pageSize.value + 1;
|
||||||
|
const MaxSize = (pageIndex.value + 1) * pageSize.value;
|
||||||
|
return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
|
||||||
}}
|
}}
|
||||||
onChange={(page, size) => {
|
onChange={(page, size) => {
|
||||||
handleSearch({
|
handleSearch({
|
||||||
...props.params,
|
...props.params,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
pageIndex: pageSize.value === size ? page : 1,
|
pageIndex: pageSize.value === size ? page : 0
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { SearchItemProps } from 'components/Search/types'
|
||||||
|
import { ColumnType } from 'ant-design-vue/es/table'
|
||||||
|
|
||||||
|
export interface JColumnsProps extends ColumnType{
|
||||||
|
scopedSlots?: boolean;
|
||||||
|
search: SearchItemProps
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ const filterPath = [
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const token = LocalStore.get(TOKEN_KEY)
|
const token = LocalStore.get(TOKEN_KEY)
|
||||||
|
// TODO 切换路由取消请求
|
||||||
if (token || filterPath.includes(to.path)) {
|
if (token || filterPath.includes(to.path)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { InstanceModel } from "@/views/device/instance/typings";
|
import { DeviceInstance, InstanceModel } from "@/views/device/instance/typings";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useInstanceStore = defineStore({
|
export const useInstanceStore = defineStore({
|
||||||
id: 'device',
|
id: 'device',
|
||||||
state: () => ({} as InstanceModel),
|
state: () => ({} as InstanceModel),
|
||||||
});
|
actions: {
|
||||||
|
setCurrent(current: Partial<DeviceInstance>) {
|
||||||
|
this.current = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { ProductItem } from "@/views/device/Product/typings";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useProductStore = defineStore({
|
||||||
|
id: 'product',
|
||||||
|
state: () => ({
|
||||||
|
current: {} as ProductItem | undefined
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setCurrent(current: ProductItem) {
|
||||||
|
this.current = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TOKEN_KEY } from '@/utils/variable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态图片资源处理
|
* 静态图片资源处理
|
||||||
* @param path {String} 路径
|
* @param path {String} 路径
|
||||||
|
@ -29,4 +31,8 @@ export const LocalStore = {
|
||||||
removeAll() {
|
removeAll() {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToken = () => {
|
||||||
|
return LocalStore.get(TOKEN_KEY)
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* 把数据下载成JSON
|
||||||
|
* @param record
|
||||||
|
* @param fileName
|
||||||
|
*/
|
||||||
|
export const downloadObject = (record: Record<string, any>, fileName: string, format?: string) => {
|
||||||
|
// 创建隐藏的可下载链接
|
||||||
|
const ghostLink = document.createElement('a');
|
||||||
|
ghostLink.download = `${fileName ? '' : record?.name}${fileName}_${moment(new Date()).format(
|
||||||
|
format || 'YYYY_MM_DD',
|
||||||
|
)}.json`;
|
||||||
|
ghostLink.style.display = 'none';
|
||||||
|
//字符串内容转成Blob地址
|
||||||
|
const blob = new Blob([JSON.stringify(record)]);
|
||||||
|
ghostLink.href = URL.createObjectURL(blob);
|
||||||
|
//触发点击
|
||||||
|
document.body.appendChild(ghostLink);
|
||||||
|
ghostLink.click();
|
||||||
|
//移除
|
||||||
|
document.body.removeChild(ghostLink);
|
||||||
|
};
|
|
@ -1,11 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='search'>
|
<div class='search'>
|
||||||
<Search />
|
<Search
|
||||||
|
:columns='columns'
|
||||||
|
/>
|
||||||
|
<Search type='simple' />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup name='demoSearch'>
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
rename: 'deviceId',
|
||||||
|
type: 'select',
|
||||||
|
handValue: (v) => {
|
||||||
|
return '123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'classifiedName',
|
||||||
|
key: 'classifiedName',
|
||||||
|
search: {
|
||||||
|
first: true,
|
||||||
|
type: 'treeSelect',
|
||||||
|
// options: async () => {
|
||||||
|
// return await
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 250,
|
||||||
|
scopedSlots: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
@cancelSelect="cancelSelect"
|
@cancelSelect="cancelSelect"
|
||||||
>
|
>
|
||||||
<template #headerTitle>
|
<template #headerTitle>
|
||||||
<a-button type="primary">新增</a-button>
|
<a-button type="primary" @click="add">新增</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template #card="slotProps">
|
<template #card="slotProps">
|
||||||
<CardBox
|
<CardBox
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</template>
|
</template>
|
||||||
<!-- <template #actions="item">
|
<template #actions="item">
|
||||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||||
<a-button :disabled="item.disabled">
|
<a-button :disabled="item.disabled">
|
||||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template> -->
|
</template>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</template>
|
</template>
|
||||||
<template #id="slotProps">
|
<template #id="slotProps">
|
||||||
|
@ -83,8 +83,10 @@ import server from "@/utils/request";
|
||||||
import type { ActionsType } from '@/components/Table/index.vue'
|
import type { ActionsType } from '@/components/Table/index.vue'
|
||||||
import { getImage } from '@/utils/comm';
|
import { getImage } from '@/utils/comm';
|
||||||
import { DeleteOutlined } from '@ant-design/icons-vue'
|
import { DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
const request = (data: any) => server.post(`/device-product/_query`, data)
|
const request = (data: any) => server.post(`/device-product/_query`, data)
|
||||||
|
// const request = (data: any) => server.post(`/device/category/_tree`, {paging: false})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
@ -152,26 +154,26 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
title: '导入'
|
title: '导入'
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
icon: 'icon-xiazai'
|
icon: 'icon-xiazai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
// disabled: true,
|
|
||||||
text: "删除",
|
text: "删除",
|
||||||
disabled: !!data?.state,
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
||||||
},
|
},
|
||||||
// popConfirm: {
|
popConfirm: {
|
||||||
// title: '确认删除?'
|
title: '确认删除?'
|
||||||
// },
|
},
|
||||||
|
|
||||||
icon: 'icon-huishouzhan'
|
icon: 'icon-huishouzhan'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = h('p', 'hi')
|
const add = () => {
|
||||||
|
message.warn('123')
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</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请求头可编辑表格 -->
|
<!-- webhook请求头可编辑表格 -->
|
||||||
<template>
|
<template>
|
||||||
<a-table
|
<div class="table-wrapper">
|
||||||
:columns="columns"
|
<a-table
|
||||||
:data-source="dataSource"
|
:columns="columns"
|
||||||
bordered
|
:data-source="dataSource"
|
||||||
:pagination="false"
|
bordered
|
||||||
>
|
:pagination="false"
|
||||||
<template #bodyCell="{ column, text, record }">
|
>
|
||||||
<template v-if="['KEY', 'VALUE'].includes(column.dataIndex)">
|
<template #bodyCell="{ column, text, record }">
|
||||||
<a-input v-model="record[column.dataIndex]" />
|
<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>
|
||||||
<template v-else-if="column.dataIndex === 'operation'">
|
</a-table>
|
||||||
<a-button type="text">
|
<a-button
|
||||||
<template #icon>
|
type="dashed"
|
||||||
<delete-outlined @click="handleDelete(record.idx)" />
|
@click="handleAdd"
|
||||||
</template>
|
style="width: 100%; margin-top: 5px"
|
||||||
</a-button>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<plus-outlined />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
添加
|
||||||
</a-table>
|
</a-button>
|
||||||
<a-button
|
</div>
|
||||||
type="dashed"
|
|
||||||
@click="handleAdd"
|
|
||||||
style="width: 100%; margin-top: 5px"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<plus-outlined />
|
|
||||||
</template>
|
|
||||||
添加
|
|
||||||
</a-button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
// import { cloneDeep } from 'lodash-es';
|
import { PropType } from 'vue';
|
||||||
// import { defineComponent, reactive, ref } from 'vue';
|
import { IHeaders } from '../../types';
|
||||||
// import type { UnwrapRef } from 'vue';
|
|
||||||
|
|
||||||
interface DataItem {
|
type Emits = {
|
||||||
idx: number;
|
(e: 'update:headers', data: IHeaders[]): void;
|
||||||
KEY: string;
|
};
|
||||||
VALUE: string;
|
const emit = defineEmits<Emits>();
|
||||||
}
|
|
||||||
|
|
||||||
const data: DataItem[] = [];
|
const props = defineProps({
|
||||||
for (let i = 0; i < 2; i++) {
|
headers: {
|
||||||
data.push({
|
type: Array as PropType<IHeaders[]>,
|
||||||
idx: i,
|
default: () => [],
|
||||||
KEY: `key ${i}`,
|
},
|
||||||
VALUE: `value${i}`,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'KEY',
|
title: 'KEY',
|
||||||
dataIndex: 'KEY',
|
dataIndex: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VALUE',
|
title: 'VALUE',
|
||||||
dataIndex: 'VALUE',
|
dataIndex: 'value',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
@ -69,17 +67,20 @@ const columns = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataSource = ref(data);
|
const dataSource = computed({
|
||||||
console.log('dataSource: ', dataSource.value);
|
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);
|
dataSource.value.splice(idx, 1);
|
||||||
};
|
};
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
dataSource.value.push({
|
dataSource.value.push({
|
||||||
idx: dataSource.value.length + 1,
|
id: dataSource.value.length,
|
||||||
KEY: `key ${dataSource.value.length + 1}`,
|
key: '',
|
||||||
VALUE: `value ${dataSource.value.length + 1}`,
|
value: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import './index.less';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const DingTalk = () => {
|
||||||
|
const appKey = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-message/01-AppKey.jpg',
|
||||||
|
);
|
||||||
|
const appSecret = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-message/02-AppSecret.jpg',
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
钉钉开放平台:
|
||||||
|
<a
|
||||||
|
href="https://open-dev.dingtalk.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://open-dev.dingtalk.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、AppKey</h2>
|
||||||
|
<div>
|
||||||
|
企业内部应用的唯一身份标识。在钉钉开发者后台创建企业内部应用后,系统会自动生成一对AppKey和AppSecret。
|
||||||
|
</div>
|
||||||
|
<div>获取路径:“钉钉开放平台”--“应用开发”--“应用信息”</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2、AppSecret</h2>
|
||||||
|
<div>
|
||||||
|
<div>钉钉应用对应的调用密钥</div>
|
||||||
|
<div>获取路径:“钉钉开放平台”--“应用开发”--“应用信息”</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appSecret} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DingTalk;
|
|
@ -0,0 +1,35 @@
|
||||||
|
.doc {
|
||||||
|
height: 750px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: rgba(#000, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
.url {
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: #2f54eb;
|
||||||
|
background-color: rgba(#a7bdf7, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 16px 0;
|
||||||
|
color: rgba(#000, 0.85);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 6px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import DingTalk from './DingTalk';
|
||||||
|
|
||||||
|
const Doc = () => {
|
||||||
|
return <DingTalk />;
|
||||||
|
};
|
||||||
|
export default Doc;
|
|
@ -259,6 +259,7 @@
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
|
:loading="btnLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
|
@ -266,14 +267,16 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" :push="2"></a-col>
|
<a-col :span="12" :push="2">
|
||||||
|
<Doc />
|
||||||
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getImage, LocalStore } from '@/utils/comm';
|
import { getImage } from '@/utils/comm';
|
||||||
import { Form } from 'ant-design-vue';
|
import { Form } from 'ant-design-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { ConfigFormData } from '../types';
|
import { ConfigFormData } from '../types';
|
||||||
|
@ -283,8 +286,12 @@ import {
|
||||||
MSG_TYPE,
|
MSG_TYPE,
|
||||||
} from '@/views/notice/const';
|
} from '@/views/notice/const';
|
||||||
import regionList from './regionId';
|
import regionList from './regionId';
|
||||||
import EditTable from './components/EditTable.vue'
|
import EditTable from './components/EditTable.vue';
|
||||||
|
import configApi from '@/api/notice/config';
|
||||||
|
import Doc from './doc/index';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
// 消息类型
|
// 消息类型
|
||||||
|
@ -311,9 +318,8 @@ const formData = ref<ConfigFormData>({
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'dingTalkMessage',
|
provider: 'dingTalkMessage',
|
||||||
type: NOTICE_METHOD[0].value,
|
type: 'dingTalk',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据通知方式展示对应的字段
|
// 根据通知方式展示对应的字段
|
||||||
watch(
|
watch(
|
||||||
() => formData.value.type,
|
() => formData.value.type,
|
||||||
|
@ -383,25 +389,56 @@ const formRules = ref({
|
||||||
pattern:
|
pattern:
|
||||||
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
|
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
|
||||||
message: 'Webhook需要是一个合法的URL',
|
message: 'Webhook需要是一个合法的URL',
|
||||||
trigger: 'blur',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetFields, validate, validateInfos } = useForm(
|
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||||
formData.value,
|
formData.value,
|
||||||
formRules.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 = () => {
|
const handleSubmit = () => {
|
||||||
validate()
|
validate()
|
||||||
.then(async () => {})
|
.then(async () => {
|
||||||
.catch((err) => {});
|
// 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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,20 @@
|
||||||
<div class="page-container">通知配置</div>
|
<div class="page-container">通知配置</div>
|
||||||
</template>
|
</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>
|
<style lang="less" scoped></style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
interface IHeaders {
|
export interface IHeaders {
|
||||||
|
id?: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
@ -34,4 +35,8 @@ export type ConfigFormData = {
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
id?: string;
|
||||||
|
maxRetryTimes?: number;
|
||||||
|
creatorId?: string;
|
||||||
|
createTime?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,8 +5,16 @@
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<img
|
<img
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
:src="getImage('/login.png')"
|
:src="basis.backgroud || getImage('/login.png')"
|
||||||
/>
|
/>
|
||||||
|
<a
|
||||||
|
href="https://beian.miit.gov.cn/#/Integrated/index"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="records"
|
||||||
|
>
|
||||||
|
备案:渝ICP备19017719号-1
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -16,11 +24,13 @@
|
||||||
<img
|
<img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
class="logo"
|
class="logo"
|
||||||
:src="getImage('/logo.png')"
|
:src="basis.logo || getImage('/logo.png')"
|
||||||
/>
|
/>
|
||||||
<!-- </link> -->
|
<!-- </link> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="desc">物联网平台</div>
|
<div class="desc">
|
||||||
|
{{ basis.title || SystemConst.SYSTEM_NAME }}
|
||||||
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<a-form
|
<a-form
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
|
@ -168,11 +178,13 @@ import {
|
||||||
getInitSet,
|
getInitSet,
|
||||||
systemVersion,
|
systemVersion,
|
||||||
bindInfo,
|
bindInfo,
|
||||||
|
settingDetail,
|
||||||
} from '@/api/login';
|
} from '@/api/login';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
import { LocalStore } from '@/utils/comm';
|
import { LocalStore } from '@/utils/comm';
|
||||||
import { BASE_API_PATH, TOKEN_KEY, Version_Code } from '@/utils/variable';
|
import { BASE_API_PATH, TOKEN_KEY, Version_Code } from '@/utils/variable';
|
||||||
|
import { SystemConst } from '@/utils/consts';
|
||||||
|
|
||||||
const store = useUserInfo();
|
const store = useUserInfo();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -200,6 +212,7 @@ const codeConfig = ref(false);
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const bindings = ref<any[]>();
|
const bindings = ref<any[]>();
|
||||||
|
const basis = ref<any>({});
|
||||||
|
|
||||||
const defaultImg = getImage('/apply/provider1.png');
|
const defaultImg = getImage('/apply/provider1.png');
|
||||||
const iconMap = new Map();
|
const iconMap = new Map();
|
||||||
|
@ -222,6 +235,15 @@ const onFinish = async () => {
|
||||||
username: form.username,
|
username: form.username,
|
||||||
});
|
});
|
||||||
LocalStore.set(TOKEN_KEY, res?.result.token);
|
LocalStore.set(TOKEN_KEY, res?.result.token);
|
||||||
|
// if (res.result.username === 'admin') {
|
||||||
|
// const resp: any = await getInitSet();
|
||||||
|
// if (resp.status === 200 && !resp.result.length) {
|
||||||
|
// window.location.href = '/#/init-home';
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// window.location.href = '/';
|
||||||
|
|
||||||
const resp: any = await getInitSet();
|
const resp: any = await getInitSet();
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
router.push('/demo');
|
router.push('/demo');
|
||||||
|
@ -257,6 +279,7 @@ const getCookie = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOpen = () => {
|
const getOpen = () => {
|
||||||
|
LocalStore.removeAll();
|
||||||
systemVersion().then((res: any) => {
|
systemVersion().then((res: any) => {
|
||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
LocalStore.set(Version_Code, res.result.edition);
|
LocalStore.set(Version_Code, res.result.edition);
|
||||||
|
@ -269,6 +292,18 @@ const getOpen = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
settingDetail('front').then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
const ico: any = document.querySelector('link[rel="icon"]');
|
||||||
|
ico.href = res.result.ico;
|
||||||
|
basis.value = res.result;
|
||||||
|
if (res.result.title) {
|
||||||
|
document.title = res.result.title;
|
||||||
|
} else {
|
||||||
|
document.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOther = (item: any) => {
|
const handleClickOther = (item: any) => {
|
||||||
|
@ -316,6 +351,14 @@ screenRotation(screenWidth.value, screenHeight.value);
|
||||||
.left {
|
.left {
|
||||||
width: 73%;
|
width: 73%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.records {
|
||||||
|
position: absolute;
|
||||||
|
top: 96%;
|
||||||
|
left: 35%;
|
||||||
|
color: rgba(0, 0, 0, 0.35);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
|
|
Loading…
Reference in New Issue