Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-03-12 15:47:21 +08:00
commit 08ca907b99
185 changed files with 13471 additions and 9721 deletions

View File

@ -100,7 +100,7 @@ const matchComponents: IMatcher[] = [
},
{
pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'TimeTicker'
styleDir: 'TimePicker'
},
{
pattern: /^Radio/,

View File

@ -15,7 +15,7 @@ export const unBind_api = (appId: string) => server.post(`/application/sso/${app
* @param type
* @param name
*/
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`,name,{
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`,name,{},{
headers: {
'Content-Type': 'text/plain'
}
@ -24,7 +24,7 @@ export const validateField_api = (type: 'username' | 'password', name: string)
*
* @param password
*/
export const checkOldPassword_api = (password:string) => server.post(`/user/me/password/_validate`,password,{
export const checkOldPassword_api = (password:string) => server.post(`/user/me/password/_validate`,password,{},{
headers: {
'Content-Type': 'text/plain'
}

View File

@ -1,7 +1,7 @@
import server from '@/utils/request'
// 获取记录列表
export const getList_api = (data:object): any =>server.get(`/notifications/_query`,encodeParams(data))
export const getList_api = (data:object): any =>server.post(`/notifications/_query`,data)
// 修改记录状态
export const changeStatus_api = (type:'_read'|'_unread',data:string[]): any =>server.post(`/notifications/${type}`,data)

View File

@ -85,7 +85,7 @@ export const batchDeleteDevice = (data: string[]) => server.put(`/device-instanc
*/
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
export const templateDownload = (productId: string, type: string) => server.get(`/device-instance/${productId}/template.${type}`,{},{responseType: 'blob'})
export const templateDownload = (productId: string, type: string) => server.get(`/device-instance/${productId}/template.${type}`, {}, { responseType: 'blob' })
/**
*
* @param productId id
@ -100,7 +100,7 @@ export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boo
* @param type
* @returns
*/
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}`
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`
/**
* ID是否重复
@ -245,6 +245,22 @@ export const unbindBatchDevice = (deviceId: string, data: Record<string, any>) =
*/
export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data)
/**
*
* @param data
*/
export const getProductListNoPage = (data: any) => server.post('/device/product/_query/no-paging?paging=false', data)
/**
*
*/
export const editDevice = (parmas: any) => server.patch('/device-instance', parmas)
/**
*
*/
export const addDevice = (params: any) => server.post("/device-instance", params)
/**
*
* @param id id
@ -504,14 +520,14 @@ export const productCode = (productId: string) => server.get(`/device/transparen
* @param productId
* @returns
*/
export const saveProductCode = (productId: string,data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}`,data)
export const saveProductCode = (productId: string, data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}`, data)
/**
*
* @param productId
* @param deviceId
* @returns
*/
export const deviceCode = (productId: string,deviceId:string) => server.get(`device/transparent-codec/${productId}/${deviceId}`)
export const deviceCode = (productId: string, deviceId: string) => server.get(`device/transparent-codec/${productId}/${deviceId}`)
/**
*
* @param productId
@ -520,13 +536,13 @@ export const deviceCode = (productId: string,deviceId:string) => server.get(`dev
* @param data
* @returns
*/
export const saveDeviceCode = (productId: string,deviceId:string,data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}/${deviceId}`,data)
export const saveDeviceCode = (productId: string, deviceId: string, data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}/${deviceId}`, data)
/**
*
* @param data
* @returns
*/
export const testCode = (data: Record<string, unknown>) => server.post(`/device/transparent-codec/decode-test`,data)
export const testCode = (data: Record<string, unknown>) => server.post(`/device/transparent-codec/decode-test`, data)
/**
*
* @param productId

View File

@ -16,6 +16,9 @@ export default {
// 删除
del: (id: string) => server.remove(`/media/channel/${id}`),
// 查询树形数据
queryTree: (id: string, data?: any) => server.post(`/media/device/${id}/catalog/_query/tree`, data),
// ========== 视频播放 ==========
// 开始直播
ptzStart: (deviceId: string, channelId: string, type: string) =>

View File

@ -7,7 +7,7 @@ export const getUserType_api = () => server.get(`/user/detail/types`);
export const getUserList_api = (data: object) => server.post(`/user/detail/_query`, data);
// 校验字段合法性
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name, {
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name,{}, {
headers: {
'Content-Type': 'text/plain'
}
@ -25,7 +25,7 @@ export const addUser_api = (data: object) => server.post(`/user/detail/_create`,
// 更新用户
export const updateUser_api = (data: any) => server.put(`/user/detail/${data.id}/_update`, data);
// 更新密码
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password, {
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password,{}, {
headers: {
'Content-Type': 'text/plain'
}

View File

@ -77,6 +77,7 @@ const iconKeys = [
'CloudDownloadOutlined',
'PauseCircleOutlined',,
'FormOutlined',
'EyeInvisibleOutlined',
]
const Icon = (props: {type: string}) => {

View File

@ -15,7 +15,7 @@
<a-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<a-input v-model:value="record.id" size="small"></a-input>
<j-auto-complete :options="options" v-model:value="record.id" size="small" width="130px"/>
</template>
<template v-if="column.key === 'current'">
<a-input v-model:value="record.current" size="small"></a-input>
@ -58,8 +58,8 @@
</div>
<div class="log">
<a-descriptions>
<a-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')" :key="item.time"
:span="3">
<a-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')"
:key="item.time" :span="3">
<a-tooltip placement="top" :title="item.content">
{{ item.content }}
</a-tooltip>
@ -78,6 +78,7 @@ import { message } from 'ant-design-vue';
import { useRuleEditorStore } from '@/store/ruleEditor';
import moment from 'moment';
import { getWebSocket } from '@/utils/websocket';
import { PropertyMetadata } from '@/views/device/Product/typings';
const props = defineProps({
@ -135,25 +136,25 @@ const runScript = () => {
});
if (ws.value) {
ws.value.unsubscribe();
ws.value.unsubscribe?.();
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
message.warning('请编辑规则');
return;
}
ws.value = getWebSocket(`virtual-property-debug-${ruleEditorStore.state.property}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: ruleEditorStore.state.property,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
})
}
const beginAction = () => {
@ -163,7 +164,7 @@ const beginAction = () => {
const stopAction = () => {
isBeginning.value = true;
if (ws.value) {
ws.value.unsubscribe();
ws.value.unsubscribe?.();
}
}
const clearAction = () => {
@ -172,9 +173,21 @@ const clearAction = () => {
onUnmounted(() => {
if (ws.value) {
ws.value.unsubscribe();
ws.value.unsubscribe?.();
}
})
const options = ref<{ label: string, value: string }[]>()
const getProperty = () => {
const metadata = productStore.current.metadata || '{}';
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
options.value = _p.filter((p) => p.id !== props.id).map((item) => ({
label: item.name,
value: item.id,
}));
console.log(options.value)
}
getProperty()
</script>
<style lang="less" scoped>
.debug-container {

View File

@ -1,6 +1,5 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
{{ _value }}
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
</template>

View File

@ -2,36 +2,36 @@
<div class="indicator-box">
<template v-if="['int', 'long', 'double', 'float'].includes(type)">
<template v-if="value.range">
<a-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></a-input-number>
<j-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></j-input-number>
~
<a-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></a-input-number>
<j-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></j-input-number>
</template>
<a-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></a-input-number>
<j-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></j-input-number>
</template>
<template v-else-if="type === 'date'">
<a-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
<a-date-picker v-else show-time v-model:value="value.value" size="small" />
<j-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
<j-date-picker v-else show-time v-model:value="value.value" size="small" />
</template>
<template v-else-if="type === 'boolean'">
<a-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></a-select>
<j-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></j-select>
</template>
<template v-else-if="type === 'string'">
<a-input v-model:value="value.value" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value" size="small" placeholder="请输入"></j-input>
</template>
<template v-else>
<template v-if="value.range">
<a-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></j-input>
~
<a-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></j-input>
</template>
<a-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></a-input-number>
<j-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></j-input-number>
</template>
<div v-if="type !== 'boolean' && type !== 'string'">
<a-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
<j-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
范围
</a-checkbox>
</j-checkbox>
</div>
</div>
</template>

View File

@ -26,7 +26,7 @@ import { notification } from 'ant-design-vue';
import { changeStatus_api } from '@/api/account/notificationRecord';
import { useUserInfo } from '@/store/userInfo';
const updateCount = computed(()=>useUserInfo().$state.alarmUpdateCount);
const updateCount = computed(() => useUserInfo().$state.alarmUpdateCount);
const total = ref(0);
const list = ref<any[]>([]);
@ -50,10 +50,20 @@ const subscribeNotice = () => {
const getList = () => {
loading.value = true;
const params = {
'terms[0].column': 'state',
'terms[0].value': 'unread',
'sorts[0].name': 'notifyTime',
'sorts[0].order': 'desc',
terms: [
{
terms: [
{
type: 'or',
value: 'unread',
termType: 'eq',
column: 'state',
},
],
},
],
};
getList_api(params)
.then((resp: any) => {

View File

@ -1,28 +1,28 @@
<template>
<a-popover :visible="visible" placement="left">
<j-popover :visible="visible" placement="left">
<template #title>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置元素</div>
<AIcon type="CloseOutlined" @click="visible = false" />
<div @click="visible = false"><AIcon type="CloseOutlined" /></div>
</div>
</template>
<template #content>
<div style="max-width: 400px;">
<div class="ant-form-vertical">
<value-type-form v-model:value="_value" :name="name" isSub key="sub"></value-type-form>
<a-form-item label="说明" :name="name.concat(['description'])" :rules="[
<j-form-item label="说明" :name="name.concat(['description'])" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="_value.description" size="small"></a-textarea>
</a-form-item>
<j-textarea v-model:value="_value.description" size="small"></j-textarea>
</j-form-item>
</div>
</div>
</template>
<a-button type="dashed" block @click="visible = true">
<j-button type="dashed" block @click="visible = true">
配置元素
<AIcon type="EditOutlined" class="item-icon" />
</a-button>
</a-popover>
</j-button>
</j-popover>
</template>
<script setup lang="ts" name="ArrayParam">
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
@ -33,7 +33,7 @@ type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({ extends: {} })
default: () => ({ expands: {} })
},
name: {
type: Array as PropType<(string | number)[]>,
@ -55,7 +55,7 @@ const _value = computed({
const visible = ref(false)
onMounted(() => {
emit('update:value', { extends: {}, ...props.value })
emit('update:value', { expands: {}, ...props.value })
})
</script>
<style lang="less" scoped>

View File

@ -1,35 +1,35 @@
<template>
<div class="boolean-param">
<a-row :gutter="4">
<a-col :span="12">
<a-form-item label=" " :name="name.concat(['trueText'])" :rules="[
<j-row :gutter="4">
<j-col :span="12">
<j-form-item label=" " :name="name.concat(['trueText'])" :rules="[
{ required: true, message: '请输入trueText' },
]">
<a-input v-model:value="value.trueText" placeholder="trueText" size="small" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="-" :name="name.concat(['trueValue'])" :rules="[
<j-input v-model:value="value.trueText" placeholder="trueText" size="small" />
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item label="-" :name="name.concat(['trueValue'])" :rules="[
{ required: true, message: '请输入trueValue' },
]">
<a-input v-model:value="value.trueValue" placeholder="trueValue" size="small"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label=" " :name="name.concat(['falseText'])" :rules="[
<j-input v-model:value="value.trueValue" placeholder="trueValue" size="small"/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item label=" " :name="name.concat(['falseText'])" :rules="[
{ required: true, message: '请输入falseText' },
]">
<a-input v-model:value="value.falseText" placeholder="falseText" size="small" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="-" :name="name.concat(['falseValue'])" :rules="[
<j-input v-model:value="value.falseText" placeholder="falseText" size="small" />
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item label="-" :name="name.concat(['falseValue'])" :rules="[
{ required: true, message: '请输入falseValue' },
]">
<a-input v-model:value="value.falseValue" placeholder="falseValue" size="small" />
</a-form-item>
</a-col>
</a-row>
<j-input v-model:value="value.falseValue" placeholder="falseValue" size="small" />
</j-form-item>
</j-col>
</j-row>
</div>
</template>
<script setup lang="ts" name="BooleanParam">

View File

@ -1,5 +1,5 @@
<template>
<a-popover placement="left" trigger="click">
<j-popover placement="left" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">{{ config.name }}</div>
@ -7,18 +7,18 @@
</template>
<template #content>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
<a-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
<j-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
<j-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
label: e.text,
value: e.value,
}))" size="small"></a-select>
</a-form-item>
}))" size="small"></j-select>
</j-form-item>
</div>
</template>
<a-button type="dashed" block>
<j-button type="dashed" block>
存储配置<AIcon type="EditOutlined" class="item-icon"/>
</a-button>
</a-popover>
</j-button>
</j-popover>
</template>
<script setup lang="ts" name="ConfigParam">
import { PropType } from 'vue';

View File

@ -5,43 +5,45 @@
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top">
<j-popover :visible="editIndex === index" placement="top">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">枚举项配置</div>
<AIcon type="CloseOutlined" @click="handleClose" />
<div @click="handleClose"><AIcon type="CloseOutlined" /></div>
</div>
</template>
<template #content>
<div class="ant-form-vertical">
<a-form-item label="Value" :name="name.concat([index, 'value'])" :rules="[
<j-form-item label="Value" :name="name.concat([index, 'value'])" :rules="[
{ required: true, message: '请输入Value' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].value" size="small"></a-input>
</a-form-item>
<a-form-item label="Text" :name="name.concat([index, 'text'])" :rules="[
<j-input v-model:value="_value[index].value" size="small"></j-input>
</j-form-item>
<j-form-item label="Text" :name="name.concat([index, 'text'])" :rules="[
{ required: true, message: '请输入Text' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].text" size="small"></a-input>
</a-form-item>
<j-input v-model:value="_value[index].text" size="small"></j-input>
</j-form-item>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.text || '枚举项配置' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
新增枚举型
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="BooleanParam">

View File

@ -5,16 +5,16 @@
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="left">
<j-popover :visible="editIndex === index" placement="left">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置参数</div>
<AIcon type="CloseOutlined" @click="handleClose" />
<div @click="handleClose"><AIcon type="CloseOutlined" /></div>
</div>
</template>
<template #content>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
<j-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -22,14 +22,14 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
<j-input v-model:value="_value[index].id" size="small"></j-input>
</j-form-item>
<j-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<j-input v-model:value="_value[index].name" size="small"></j-input>
</j-form-item>
<value-type-form v-model:value="_value[index].valueType" :name="name.concat([index, 'valueType'])" isSub
key="json_sub"></value-type-form>
</div>
@ -38,18 +38,18 @@
{{ item.name || '配置参数' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
<div class="item-right" @click="handleDelete(index)">
<AIcon type="DeleteOutlined" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加参数
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="JsonParam">
@ -96,6 +96,7 @@ const handleDelete = (index: number) => {
_value.value.splice(index, 1)
}
const handleClose = () => {
console.log(editIndex.value)
editIndex.value = -1
}
const handleAdd = () => {

View File

@ -6,7 +6,7 @@
{{ `#${index + 1}.` }}
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
<j-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置参数</div>
@ -16,7 +16,7 @@
<template #content>
<div>
<div class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
<j-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -24,20 +24,19 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
<j-input v-model:value="_value[index].id" size="small"></j-input>
</j-form-item>
<j-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<a-form-item label="指标值" :name="name.concat([index, 'value'])" :rules="[
{ required: true, message: '请输入指标值' },
{ validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
<j-input v-model:value="_value[index].name" size="small"></j-input>
</j-form-item>
<j-form-item label="指标值" :name="name.concat([index, 'value'])" :rules="[
{ required: true, validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
]">
<JIndicators v-model:value="_value[index]" :type="type" size="small" :enum="enum" />
</a-form-item>
</j-form-item>
</div>
</div>
</template>
@ -45,18 +44,18 @@
{{ item.name || '配置参数' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加指标
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="MetricsParam">

View File

@ -1,33 +1,33 @@
<template>
<a-form-item :name="name.concat(['script'])">
<f-rule-editor v-model:value="value.script" :id="id"></f-rule-editor>
</a-form-item>
<j-form-item :name="name.concat(['script'])">
<f-rule-editor v-model:value="value.script" :id="id" :virtualRule="value"></f-rule-editor>
</j-form-item>
<template v-if="showWindow">
<a-form-item label="规则配置" :name="name.concat(['isVirtualRule'])">
<a-switch v-model:checked="value.isVirtualRule" :checked-value="true" :un-checked-value="false"
@change="changeWindow"></a-switch>
</a-form-item>
<j-form-item label="规则配置" :name="name.concat(['isVirtualRule'])">
<j-switch v-model:checked="value.isVirtualRule" :checked-value="true" :un-checked-value="false"
@change="changeWindow"></j-switch>
</j-form-item>
<template v-if="value.isVirtualRule">
<a-form-item label="窗口" :name="name.concat(['windowType'])" :rules="[
<j-form-item label="窗口" :name="name.concat(['windowType'])" :rules="[
{ required: true, message: '请选择窗口' },
]">
<a-select v-model:value="value.windowType" :options="windowTypeEnum" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item label="聚合函数" :name="name.concat(['aggType'])" :rules="[
<j-select v-model:value="value.windowType" :options="windowTypeEnum" size="small" allow-clear></j-select>
</j-form-item>
<j-form-item label="聚合函数" :name="name.concat(['aggType'])" :rules="[
{ required: true, message: '请选择聚合函数' },
]">
<a-select v-model:value="value.aggType" :options="aggTypeOptions" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
<j-select v-model:value="value.aggType" :options="aggTypeOptions" size="small" allow-clear></j-select>
</j-form-item>
<j-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
{ required: true, message: '请输入窗口长度' },
]">
<a-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
<a-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
<j-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
<j-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
{ required: true, message: '请输入步长' },
]">
<a-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
<j-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
</template>
</template>
</template>

View File

@ -5,7 +5,7 @@
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY)
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
accept=".xlsx,.csv"
:maxCount="1"
@ -26,19 +26,23 @@
</a-space>
<div style="margin-top: 20px" v-if="importLoading">
<a-badge v-if="flag" status="processing" text="进行中" />
<a-badge v-else status="success" text="已完成" />
<span>总数量{{count}}</span>
<p style="color: red">{{errMessage}}</p>
<a-badge v-else status="success" text="已完成" />
<span>总数量{{ count }}</span>
<p style="color: red">{{ errMessage }}</p>
</div>
</template>
<script lang="ts" setup>
import { FILE_UPLOAD } from '@/api/comm'
import { TOKEN_KEY } from '@/utils/variable';
import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { downloadFile, downloadFileByUrl } from '@/utils/utils';
import { deviceImport, deviceTemplateDownload ,templateDownload} from '@/api/device/instance'
import { EventSourcePolyfill } from 'event-source-polyfill'
import {
deviceImport,
deviceTemplateDownload,
templateDownload,
} from '@/api/device/instance';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { message } from 'ant-design-vue';
type Emits = {
@ -50,11 +54,11 @@ const props = defineProps({
//
modelValue: {
type: Array,
default: () => []
default: () => [],
},
product: {
type: String,
default: ''
default: '',
},
file: {
type: Object,
@ -62,67 +66,62 @@ const props = defineProps({
return {
fileType: 'xlsx',
autoDeploy: false,
}
}
}
})
};
},
},
});
const importLoading = ref<boolean>(false)
const flag = ref<boolean>(false)
const count = ref<number>(0)
const errMessage = ref<string>('')
const importLoading = ref<boolean>(false);
const flag = ref<boolean>(false);
const count = ref<number>(0);
const errMessage = ref<string>('');
const downFile =async (type: string) => {
const downFile = async (type: string) => {
// downloadFile(deviceTemplateDownload(props.product, type));
const res:any =await templateDownload(props.product, type)
if(res){
const res: any = await templateDownload(props.product, type);
if (res) {
const blob = new Blob([res], { type: type });
const url = URL.createObjectURL(blob);
console.log(url);
downloadFileByUrl(
url,
`设备导入模版`,
type,
);
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备导入模版`, type);
}
}
};
const submitData = async (fileUrl: string) => {
if (!!fileUrl) {
count.value = 0
errMessage.value = ''
flag.value = true
const autoDeploy = !!props?.file?.autoDeploy || false;
importLoading.value = true
let dt = 0;
const source = new EventSourcePolyfill(deviceImport(props.product, fileUrl, autoDeploy));
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt
} else {
errMessage.value = res.message || '失败'
}
};
source.onerror = (e: { status: number; }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员'
flag.value = false
source.close();
};
source.onopen = () => {};
count.value = 0;
errMessage.value = '';
flag.value = true;
const autoDeploy = !!props?.file?.autoDeploy || false;
importLoading.value = true;
let dt = 0;
const source = new EventSourcePolyfill(
deviceImport(props.product, fileUrl, autoDeploy),
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message || '失败';
}
};
source.onerror = (e: { status: number }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
flag.value = false;
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件')
message.error('请先上传文件');
}
}
};
const uploadChange = async (info: Record<string, any>) => {
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
}
};
</script>

View File

@ -4,32 +4,32 @@
<div class="live-player-content">
<!-- 工具栏 -->
<div class="player-screen-tool" v-if="showScreen">
<a-radio-group
<j-radio-group
:value="screen"
button-style="solid"
@change="handleScreenChange"
>
<a-radio-button :value="1">单屏</a-radio-button>
<a-radio-button :value="4">四分屏</a-radio-button>
<a-radio-button :value="9">九分屏</a-radio-button>
<a-radio-button :value="0">全屏</a-radio-button>
</a-radio-group>
<j-radio-button :value="1">单屏</j-radio-button>
<j-radio-button :value="4">四分屏</j-radio-button>
<j-radio-button :value="9">九分屏</j-radio-button>
<j-radio-button :value="0">全屏</j-radio-button>
</j-radio-group>
<div class="screen-tool-save">
<a-tooltip title="可保存分屏配置记录">
<j-tooltip title="可保存分屏配置记录">
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
<a-popover
</j-tooltip>
<j-popover
v-model:visible="visible"
trigger="click"
title="分屏名称"
>
<template #content>
<a-form
<j-form
ref="formRef"
:model="formData"
layout="vertical"
>
<a-form-item
<j-form-item
name="name"
:rules="[
{
@ -42,37 +42,37 @@
},
]"
>
<a-textarea v-model:value="formData.name" />
</a-form-item>
<a-button
<j-textarea v-model:value="formData.name" />
</j-form-item>
<j-button
type="primary"
@click="saveHistory"
:loading="loading"
style="width: 100%; margin-top: 16px"
>
保存
</a-button>
</a-form>
</j-button>
</j-form>
</template>
<a-dropdown-button
<j-dropdown-button
type="primary"
@click="visible = true"
>
保存
<template #overlay>
<a-menu>
<a-empty
<j-menu>
<j-empty
v-if="!historyList.length"
description="暂无数据"
/>
<a-menu-item
<j-menu-item
v-for="(item, index) in historyList"
:key="`his${index}`"
@click="handleHistory(item)"
>
<a-space>
<j-space>
<span>{{ item.name }}</span>
<a-popconfirm
<j-popconfirm
title="确认删除?"
ok-text="确认"
cancel-text="取消"
@ -89,13 +89,13 @@
e?.stopPropagation()
"
/>
</a-popconfirm>
</a-space>
</a-menu-item>
</a-menu>
</j-popconfirm>
</j-space>
</j-menu-item>
</j-menu>
</template>
</a-dropdown-button>
</a-popover>
</j-dropdown-button>
</j-popover>
</div>
</div>
<!-- 播放器 -->

View File

@ -11,7 +11,7 @@
:muted="'muted' in props ? props.muted !== false : true"
:hide-big-play-button="true"
:poster="props.poster || ''"
:timeout="props.timeout || 20"
:timeout="props.timeout || 30"
:video-url="url || ''"
@play="props.onPlay"
@pause="props.onPause"

View File

@ -16,10 +16,11 @@
disabled && myValue === item.value
? 'active-checked-disabled'
: '',
item.disabled ? 'disabled' : '',
]"
v-for="(item, index) in options"
:key="index"
@click="myValue = item.value"
@click="handleRadio(item)"
>
<img v-if="item.logo" class="img" :src="item.logo" alt="" />
<span>{{ item.label }}</span>
@ -86,6 +87,11 @@ const myValue = computed({
}
},
});
const handleRadio = (item: any) => {
if (item.disabled) return;
myValue.value = item.value;
};
</script>
<style lang="less" scoped>
@ -93,6 +99,11 @@ const myValue = computed({
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.disabled {
color: rgba(0, 0, 0, 0.25);
border-color: #f5f5f5;
cursor: not-allowed;
}
&-item {
width: 49%;
height: 70px;

View File

@ -7,6 +7,15 @@
:options="options"
allowClear
style="width: 100%"
@change='selectChange'
/>
<j-time-picker
v-else-if="typeMap.get(itemType) === 'time'"
v-model:value="myValue"
allowClear
format="HH:mm:ss"
style="width: 100%"
@change='timeChange'
/>
<j-date-picker
v-else-if="typeMap.get(itemType) === 'date'"
@ -16,20 +25,23 @@
lang="cn"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
@change='dateChange'
/>
<j-input-number
v-else-if="typeMap.get(itemType) === 'inputNumber'"
v-model:value="myValue"
allowClear
style="width: 100%"
@change='inputChange'
/>
<j-input
allowClear
v-else-if="typeMap.get(itemType) === 'object'"
v-model:value="myValue"
@change='inputChange'
>
<template #addonAfter>
<form-outlined @click="modalVis = true" />
<AIcon type="FormOutlined" @click="modalVis = true" />
</template>
</j-input>
<GeoComponent
@ -50,7 +62,7 @@
:showUploadList="false"
@change="handleFileChange"
>
<cloud-upload-outlined />
<AIcon type="CloudUploadOutlined" />
</j-upload>
</template>
</j-input>
@ -60,6 +72,7 @@
type="text"
v-model:value="myValue"
style="width: 100%"
@change='inputChange'
/>
<!-- 代码编辑器弹窗 -->
@ -92,6 +105,7 @@ import { FILE_UPLOAD } from '@/api/comm';
type Emits = {
(e: 'update:modelValue', data: string | number | boolean): void;
(e: 'change', data: any, item?: any): void;
};
const emit = defineEmits<Emits>();
@ -169,6 +183,23 @@ const handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => {
emit('update:modelValue', url);
}
};
const selectChange = (e: string, option: any) => {
emit('change', e, option)
}
const timeChange = (e: any) => {
emit('change', e)
}
const inputChange = (e: any) => {
emit('change', e.target.value)
}
const dateChange = (e: any) => {
emit('change', e)
}
</script>
<style lang="less" scoped></style>

View File

@ -1,5 +1,5 @@
import type { App } from 'vue'
import AIcon from './AIcon'
// import AIcon from './AIcon'
import PermissionButton from './PermissionButton/index.vue'
import JTable from './Table/index'
import TitleComponent from "./TitleComponent/index.vue";
@ -10,11 +10,12 @@ import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JProUpload from './JUpload/index.vue'
import { BasicLayoutPage, BlankLayoutPage } from './Layout'
import { PageContainer } from 'jetlinks-ui-components'
import { PageContainer, AIcon } from 'jetlinks-ui-components'
import Ellipsis from './Ellipsis/index.vue'
import JEmpty from './Empty/index.vue'
import AMapComponent from './AMapComponent/index.vue'
import PathSimplifier from './AMapComponent/PathSimplifier.vue'
import ValueItem from './ValueItem/index.vue'
export default {
install(app: App) {
@ -35,5 +36,6 @@ export default {
.component('JEmpty', JEmpty)
.component('AMapComponent', AMapComponent)
.component('PathSimplifier', PathSimplifier)
.component('ValueItem', ValueItem)
}
}

3
src/global.d.ts vendored
View File

@ -11,4 +11,5 @@ declare module '*.bmp';
declare module '*.js';
declare module '*.ts';
declare module 'js-cookie';
declare module 'jetlinks-ui-components';
declare module 'jetlinks-ui-components';
declare module 'vue3-json-viewer';

View File

@ -67,10 +67,10 @@ export default [
path: '/form',
component: () => import('@/views/demo/Form.vue')
},
{
path: '/system/Api',
component: () => import('@/views/system/Platforms/index.vue')
},
// {
// path: '/system/Api',
// component: () => import('@/views/system/Platforms/index.vue')
// },
// end: 测试用, 可删除
// 初始化

View File

@ -45,4 +45,5 @@ export const SystemConst = {
GET_METADATA: 'get_metadata',
REFRESH_DEVICE: 'refresh_device',
VERSION_CODE: 'version_code',
AMAP_KEY : 'amap_key',
}

View File

@ -16,7 +16,7 @@
在特定场景下设备无法直接接入阿里云物联网平台时您可先将设备接入物联网平台再使用阿里云云云对接SDK快速构建桥接服务搭建物联网平台与阿里云物联网平台的双向数据通道
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun2.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun2.png')" />
</div>
<h1>2.配置说明</h1>
<div>
@ -26,14 +26,14 @@
</div>
<div>获取路径阿里云物联网平台--服务地址</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun3.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun3.png')" />
</div>
<h2> 2AccesskeyID/Secret</h2>
<div>
用于程序通知方式调用云服务费API的用户标识和秘钥获取路径阿里云管理控制台--用户头像----AccessKey管理--查看
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun1.jpg')" />
<j-image width="100%" :src="getImage('/northbound/aliyun1.jpg')" />
</div>
<h2> 3. 网桥产品</h2>
<div>
@ -44,7 +44,7 @@
将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联关联后需要进入该产品下的每一个设备的实例信息页填入对应的阿里云物联网平台设备的DeviceNameDeviceSecret进行一对一绑定
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun4.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun4.png')" />
</div>
</div>
</div>

View File

@ -1,17 +1,17 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="16">
<j-card>
<j-row :gutter="24">
<j-col :span="16">
<TitleComponent data="基本信息" />
<a-form
<j-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item
<j-row :gutter="24">
<j-col :span="24">
<j-form-item
label="名称"
name="name"
:rules="[
@ -25,14 +25,14 @@
},
]"
>
<a-input
<j-input
placeholder="请输入名称"
v-model:value="modelRef.name"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'regionId']"
:rules="[
{
@ -44,63 +44,62 @@
<template #label>
<span>
服务地址
<a-tooltip
<j-tooltip
title="阿里云内部给每台机器设置的唯一编号"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-select
<j-select
placeholder="请选择服务地址"
v-model:value="
modelRef.accessConfig.regionId
"
show-search
:filter-option="filterOption"
@blur="productChange"
>
<a-select-option
<j-select-option
v-for="item in regionsList"
:key="item.id"
:value="item.id"
:label="item.name"
>{{ item.name }}</a-select-option
>{{ item.name }}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-select>
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'instanceId']"
>
<template #label>
<span>
实例ID
<a-tooltip
<j-tooltip
title="阿里云物联网平台中的实例ID,没有则不填"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入实例ID"
v-model:value="
modelRef.accessConfig.instanceId
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'accessKeyId']"
:rules="[
{
@ -116,27 +115,27 @@
<template #label>
<span>
accessKey
<a-tooltip
<j-tooltip
title="用于程序通知方式调用云服务API的用户标识"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入accessKey"
v-model:value="
modelRef.accessConfig.accessKeyId
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'accessSecret']"
:rules="[
{
@ -152,27 +151,27 @@
<template #label>
<span>
accessSecret
<a-tooltip
<j-tooltip
title="用于程序通知方式调用云服务费API的秘钥标识"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入accessSecret"
v-model:value="
modelRef.accessConfig.accessSecret
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
name="bridgeProductKey"
:rules="{
required: true,
@ -182,44 +181,43 @@
<template #label>
<span>
网桥产品
<a-tooltip
<j-tooltip
title="物联网平台对应的阿里云产品"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-select
<j-select
placeholder="请选择网桥产品"
v-model:value="
modelRef.bridgeProductKey
"
show-search
:filter-option="filterOption"
>
<a-select-option
<j-select-option
v-for="item in aliyunProductList"
:key="item.productKey"
:value="item.productKey"
:label="item.productName"
>{{
item.productName
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
</j-select>
</j-form-item>
</j-col>
<j-col :span="24">
<p>产品映射</p>
<a-collapse
<j-collapse
v-if="modelRef.mappings.length"
:activeKey="activeKey"
@change="onCollChange"
>
<a-collapse-panel
<j-collapse-panel
v-for="(
item, index
) in modelRef.mappings"
@ -239,9 +237,9 @@
type="DeleteOutlined"
@click="delItem(index)"
/></template>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item
<j-row :gutter="24">
<j-col :span="12">
<j-form-item
label="阿里云产品"
:name="[
'mappings',
@ -254,19 +252,16 @@
'请选择阿里云产品',
}"
>
<a-select
<j-select
placeholder="请选择阿里云产品"
v-model:value="
item.productKey
"
show-search
:filter-option="
filterOption
"
>
<a-select-option
<j-select-option
v-for="i in getAliyunProductList(
item.productKey,
item?.productKey || ''
)"
:key="i.productKey"
:value="
@ -277,13 +272,13 @@
"
>{{
i.productName
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-select>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="平台产品"
:name="[
'mappings',
@ -296,36 +291,36 @@
'请选择平台产品',
}"
>
<a-select
<j-select
placeholder="请选择平台产品"
v-model:value="
item.productId
"
show-search
:filter-option="
filterOption
"
>
<a-select-option
<j-select-option
v-for="i in getPlatProduct(
item.productId,
item.productId || ''
)"
:key="i.id"
:value="item.id"
:value="i?.id"
:label="i.name"
>{{
i.name
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col :span="24">
<a-button
</j-select>
</j-form-item>
</j-col>
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
type="dashed"
style="width: 100%; margin-top: 10px"
@click="addItem"
@ -334,10 +329,10 @@
type="PlusOutlined"
style="margin-left: 2px"
/>
</a-button>
</a-col>
<a-col :span="24" style="margin-top: 20px">
<a-form-item
</j-button>
</j-col>
<j-col :span="24" style="margin-top: 20px">
<j-form-item
label="说明"
name="description"
:rules="{
@ -345,16 +340,16 @@
message: '最多输入200个字符',
}"
>
<a-textarea
<j-textarea
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</j-form-item>
</j-col>
</j-row>
</j-form>
<div v-if="type === 'edit'">
<PermissionButton
type="primary"
@ -365,12 +360,12 @@
保存
</PermissionButton>
</div>
</a-col>
<a-col :span="8">
</j-col>
<j-col :span="8">
<Doc />
</a-col>
</a-row>
</a-card>
</j-col>
</j-row>
</j-card>
</page-container>
</template>
@ -384,7 +379,7 @@ import {
queryProductList,
} from '@/api/northbound/alicloud';
import _ from 'lodash';
import { message } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
const router = useRouter();
const route = useRoute();
@ -430,10 +425,6 @@ const loading = ref<boolean>(false);
const type = ref<'edit' | 'view'>('edit');
const activeKey = ref<string[]>(['0']);
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const queryRegionsList = async () => {
const resp = await getRegionsList();
if (resp.status === 200) {
@ -503,8 +494,9 @@ const saveBtn = () => {
);
data.bridgeProductName = product?.productName || '';
loading.value = true;
const resp = await savePatch({...toRaw(modelRef), ...data});
loading.value = false;
const resp = await savePatch({...toRaw(modelRef), ...data}).finally(() => {
loading.value = false;
})
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();

View File

@ -1,11 +1,11 @@
<template>
<page-container>
<Search
<j-advanced-search
:columns="columns"
target="northbound-aliyun"
@search="handleSearch"
/>
<JTable
<JProTable
ref="instanceRef"
:columns="columns"
:request="query"
@ -13,7 +13,7 @@
:params="params"
>
<template #headerTitle>
<a-space>
<j-space>
<PermissionButton
type="primary"
@click="handleAdd"
@ -22,7 +22,7 @@
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</a-space>
</j-space>
</template>
<template #card="slotProps">
<CardBox
@ -45,20 +45,20 @@
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
网桥产品
</div>
<div>{{ slotProps?.bridgeProductName }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
<label>说明</label>
</div>
<div>{{ slotProps?.description }}</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
@ -81,13 +81,13 @@
</CardBox>
</template>
<template #state="slotProps">
<a-badge
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #action="slotProps">
<a-space>
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
@ -104,23 +104,21 @@
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</j-space>
</template>
</JTable>
</JProTable>
</page-container>
</template>
<script setup lang="ts">
import { query, _undeploy, _deploy, _delete } from '@/api/northbound/alicloud';
import type { ActionsType } from '@/components/Table/index.vue';
import type { ActionsType } from '@/views/device/Instance/typings'
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
import { useMenuStore } from 'store/menu';
const router = useRouter();
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const current = ref<Record<string, any>>({});
const menuStory = useMenuStore();
@ -149,6 +147,9 @@ const columns = [
title: '说明',
dataIndex: 'describe',
key: 'describe',
search: {
type: 'string',
},
},
{
title: '状态',

View File

@ -40,7 +40,7 @@
</template>
<script lang="ts" setup>
import { PropType } from "vue-demi";
import { PropType } from "vue";
type Emits = {

View File

@ -9,8 +9,8 @@
<j-form-item name="messageType" label="指令类型" :rules="{
required: true,
message: '请选择指令类型',
}">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption">
}" class="other">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search>
<j-select-option value="READ_PROPERTY">读取属性</j-select-option>
<j-select-option value="WRITE_PROPERTY">修改属性</j-select-option>
<j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option>
@ -22,7 +22,7 @@
required: true,
message: '请选择属性',
}">
<j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search :filter-option="filterOption">
<j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search @change="onPropertyChange">
<j-select-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select>
</j-form-item>
@ -32,7 +32,27 @@
required: true,
message: '请输入值',
}">
<j-input />
<ValueItem
v-model:modelValue="modelRef.message.value"
:itemType="property.type || property.valueType?.type || 'int'"
:options="
property.valueType?.type === 'enum'
? (property?.dataType?.elements || []).map(
(item) => {
return {
label: item?.text,
value: item?.value,
};
},
)
: property.valueType?.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</j-form-item>
</j-col>
<j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
@ -40,7 +60,7 @@
required: true,
message: '请选择功能',
}">
<j-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search :filter-option="filterOption" @change="funcChange">
<j-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search @change="funcChange">
<j-select-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select>
</j-form-item>
@ -62,10 +82,6 @@ import EditTable from './EditTable.vue'
const formRef = ref();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const props = defineProps({
actionType: {
type: String,
@ -89,10 +105,12 @@ const props = defineProps({
type Emits = {
(e: 'update:modelValue', data: any): void;
};
const emit = defineEmits<Emits>();
const modelRef = computed({
get: () => {
onPropertyChange(props.modelValue?.message?.properties)
return props.modelValue || {
messageType: undefined,
message: {
@ -107,6 +125,8 @@ const modelRef = computed({
}
})
const property = ref<any>({})
const funcChange = (val: string) => {
if(val){
const arr = props.metadata?.functions.find((item: any) => item.id === val)?.inputs || []
@ -122,6 +142,13 @@ const funcChange = (val: string) => {
}
}
const onPropertyChange = (val: string) => {
if(val){
const _item = props.metadata?.properties.find((item: any) => item.id === val)
property.value = _item?.[0] || {}
}
}
const saveBtn = () => new Promise((resolve) => {
formRef.value.validate()
.then(() => {
@ -139,4 +166,13 @@ const saveBtn = () => new Promise((resolve) => {
defineExpose({ saveBtn })
</script>
</script>
<style lang="less" scoped>
:deep(.ant-form-item){
margin-bottom: 0;
}
.other {
margin-bottom: 24px;
}
</style>

View File

@ -51,7 +51,6 @@
placeholder="请选择产品"
v-model:value="modelRef.id"
show-search
:filter-option="filterOption"
@change="productChange"
>
<j-select-option
@ -89,7 +88,6 @@
placeholder="请选择设备类型"
v-model:value="modelRef.applianceType"
show-search
:filter-option="filterOption"
@change="typeChange"
>
<j-select-option
@ -170,13 +168,10 @@
item.action
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getTypesActions(
item.action,
item.action || ''
)"
:key="i.id"
:value="i.id"
@ -218,9 +213,6 @@
item.actionType
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
value="command"
@ -261,6 +253,9 @@
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
@ -323,13 +318,10 @@
item.source
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getDuerOSProperties(
item.source,
item.source || '',
)"
:key="i.id"
:value="i.id"
@ -361,16 +353,13 @@
"
mode="tags"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getProductProperties(
item.target,
)"
:key="i.id"
:value="item.id"
:value="i.id"
>{{
i.name
}}</j-select-option
@ -381,6 +370,9 @@
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
@ -494,10 +486,6 @@ const onActionCollChange = (_key: string[]) => {
actionActiveKey.value = _key;
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const addItem = () => {
actionActiveKey.value.push(String(modelRef.actionMappings.length));
modelRef.actionMappings.push({
@ -636,8 +624,9 @@ const saveBtn = async () => {
.then(async (data: any) => {
if (tasks.every((item) => item) && data) {
loading.value = true;
const resp = await savePatch(data);
loading.value = false;
const resp = await savePatch(data).finally(() => {
loading.value = false;
})
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();

View File

@ -5,6 +5,7 @@
@ok="handleOk"
width="770px"
@cancel="emits('update:visible', false)"
:confirmLoading="loading"
>
<j-form :model="form" layout="vertical" ref="formRef">
<j-row :gutter="24">
@ -12,7 +13,10 @@
<j-form-item
label="姓名"
name="name"
:rules="[{ required: true, message: '姓名必填' }]"
:rules="[
{ required: true, message: '姓名必填' },
{ max: 64, message: '最多可输入64个字符' },
]"
>
<j-input
v-model:value="form.name"
@ -56,7 +60,16 @@
</j-row>
<j-row :gutter="24">
<j-col :span="12">
<j-form-item label="手机号">
<j-form-item
label="手机号"
name="telephone"
:rules="[
{
pattern: /^1[3456789]\d{9}$/,
message: '请输入正确手机号',
},
]"
>
<j-input
v-model:value="form.telephone"
placeholder="请输入手机号"
@ -64,7 +77,11 @@
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item label="邮箱">
<j-form-item
label="邮箱"
name="email"
:rules="[{ type: 'email',message:'邮箱不是一个有效的email' }]"
>
<j-input
v-model:value="form.email"
placeholder="请输入邮箱"
@ -87,17 +104,19 @@ const props = defineProps<{
visible: boolean;
data: userInfoType;
}>();
const loading = ref(false)
const form = ref(props.data);
const formRef = ref<FormInstance>();
const handleOk = () => {
formRef.value?.validate().then(() => {
loading.value = true
updateMeInfo_api(form.value).then((resp) => {
if (resp.status === 200) {
message.success('保存成功');
emits('ok');
emits('update:visible', false);
}
});
}).finally(()=>loading.value = false)
});
};
</script>

View File

@ -4,6 +4,7 @@
title="重置密码"
@ok="handleOk"
width="520px"
:confirmLoading="loading"
@cancel="emits('update:visible', false)"
>
<j-form :model="form" layout="vertical" ref="formRef">
@ -11,7 +12,7 @@
label="旧密码"
name="oldPassword"
:rules="[
{ required: true },
{ required: true, message: '请输入密码' },
{ validator: checkMothods.old, trigger: 'blur' },
]"
>
@ -24,7 +25,7 @@
label="密码"
name="newPassword"
:rules="[
{ required: true },
{ required: true,message:'请输入密码' },
{ validator: checkMothods.new, trigger: 'blur' },
]"
>
@ -37,7 +38,7 @@
label="确认密码"
name="confirmPassword"
:rules="[
{ required: true },
{ required: true, message: '请输入确认密码' },
{ validator: checkMothods.confirm, trigger: 'blur' },
]"
>
@ -63,6 +64,7 @@ const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
}>();
const loading = ref(false)
const formRef = ref<FormInstance>();
const form = ref<formType>({
oldPassword: '',
@ -72,7 +74,7 @@ const form = ref<formType>({
const checkMothods = {
old: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入密码');
if (!value) return Promise.reject();
try {
const resp: any = await checkOldPassword_api(value);
if (resp.status === 200 && !resp.result.passed)
@ -83,7 +85,7 @@ const checkMothods = {
}
},
new: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入密码');
if (!value) return Promise.reject();
else if (
form.value.confirmPassword &&
value !== form.value.confirmPassword
@ -99,7 +101,7 @@ const checkMothods = {
}
},
confirm: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入确认密码');
if (!value) return Promise.reject();
try {
const resp: any = await validateField_api('password', value);
@ -114,6 +116,7 @@ const checkMothods = {
const handleOk = () => {
formRef.value?.validate().then(() => {
loading.value = true
const params = {
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
@ -124,7 +127,7 @@ const handleOk = () => {
emits('ok');
emits('update:visible', false);
}
});
}).finally(()=>loading.value = false)
});
};
console.clear();

View File

@ -8,10 +8,15 @@
style="width: 350px; justify-content: center"
>
<img
v-if="userInfo.avatar"
:src="userInfo.avatar"
style="width: 140px; border-radius: 70px"
alt=""
/>
<div class="default-avatar" v-else>
<AIcon type="UserOutlined" />
</div>
<div
style="
width: 100%;
@ -29,6 +34,7 @@
}"
:action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload"
:beforeUpload="upload.beforeUpload"
>
<j-button>
<AIcon type="UploadOutlined" />
@ -51,11 +57,17 @@
</div>
<div class="info-card">
<p>注册时间</p>
<p>{{ moment(userInfo.createTime).format('YYYY-MM-DD HH:mm:ss') }}</p>
<p>
{{
moment(userInfo.createTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</p>
</div>
<div class="info-card">
<p>电话</p>
<p>{{ userInfo.telephone }}</p>
<p>{{ userInfo.telephone || '-' }}</p>
</div>
<div class="info-card">
<p>姓名</p>
@ -117,7 +129,7 @@
type="link"
@click="editPasswordVisible = true"
>
<AIcon type="EditOutlined" style="color: #1d39c4;" />
<AIcon type="EditOutlined" style="color: #1d39c4" />
</PermissionButton>
</span>
</div>
@ -205,7 +217,7 @@
<EditInfoDialog
v-if="editInfoVisible"
v-model:visible="editInfoVisible"
:data="{...userInfo}"
:data="{ ...userInfo }"
@ok="getUserInfo"
/>
<EditPasswordDialog
@ -277,6 +289,15 @@ const upload = reactive({
message.error('logo上传失败请稍后再试');
}
},
beforeUpload: ({ size, type }: File) => {
const imageTypes = ['jpg', 'png', 'jfif', 'pjp', 'pjpeg', 'jpeg'];
const typeBool =
imageTypes.filter((typeStr) => type.includes(typeStr)).length > 0;
const sizeBool = size < 4 * 1024 * 1024;
(typeBool && sizeBool) || message.error('请上传正确格式的图片');
return typeBool && sizeBool;
},
});
//
const isApiUser = ref<boolean>();
@ -346,7 +367,7 @@ function getViews() {
background-color: #f0f2f5;
min-height: 100vh;
.card {
margin: 24px;
margin: 16px 0;
padding: 24px;
background-color: #fff;
position: relative;
@ -370,6 +391,18 @@ function getViews() {
flex-wrap: wrap;
.content-item {
margin-right: 24px;
.default-avatar {
background-color: #ccc;
color: #fff;
border-radius: 50%;
font-size: 70px;
width: 140px;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
}
.info-card {
width: 25%;

View File

@ -1,14 +1,17 @@
<template>
<page-container>
<div class="notification-record-container">
<Search :columns="columns" @search="query.search" />
<j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
:columns="columns"
:request="getList_api"
model="TABLE"
:params="query.params.value"
:params="queryParams"
:defaultParams="{
'sorts[0].name': 'notifyTime',
'sorts[0].order': 'desc',
@ -52,8 +55,8 @@
? '标为未读'
: '标为已读',
}"
>1
<AIcon type="ReadIconOutlined" />
>
<AIcon type="icon-a-PIZHU1" />
</PermissionButton>
<PermissionButton
type="link"
@ -158,15 +161,10 @@ const columns = [
key: 'action',
ellipsis: true,
scopedSlots: true,
width:'200px'
width: '200px',
},
];
const query = {
params: ref({}),
search: (params: object) => {
query.params.value = { ...params };
},
};
const queryParams = ref({});
const tableRef = ref();
const table = {

View File

@ -3,6 +3,7 @@
visible
:title="props.data.id ? '编辑' : '新增'"
width="865px"
:confirmLoading="loading"
@ok="confirm"
@cancel="emits('update:visible', false)"
>
@ -92,6 +93,7 @@ const props = defineProps<{
data: rowType;
}>();
const loading = ref(false);
const initForm = {
subscribeName: '',
topicConfig: {},
@ -106,13 +108,16 @@ const form = ref({
const confirm = () => {
formRef.value &&
formRef.value.validate().then(() => {
save_api(form.value).then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('ok')
emits('update:visible', false);
}
});
loading.value = true;
save_api(form.value)
.then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('ok');
emits('update:visible', false);
}
})
.finally(() => (loading.value = false));
});
};

View File

@ -1,13 +1,16 @@
<template>
<page-container>
<div class="notification-subscription-container">
<Search :columns="columns" @search="query.search" />
<j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
:columns="columns"
:request="getNoticeList_api"
model="TABLE"
:params="query.params.value"
:params="queryParams"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
}"
@ -105,7 +108,7 @@ import EditDialog from './components/EditDialog.vue';
import {
getNoticeList_api,
changeStatus_api,
remove_api
remove_api,
} from '@/api/account/notificationSubscription';
import { rowType } from './typing';
import { message } from 'ant-design-vue';
@ -147,19 +150,14 @@ const columns = [
key: 'action',
ellipsis: true,
scopedSlots: true,
width: '200px'
width: '200px',
},
];
const query = {
params: ref({}),
search: (params: object) => {
query.params.value = {...params};
},
};
const queryParams = ref({});
const dialogVisible = ref<boolean>(false);
const tableRef = ref();
const table = {
seletctRow: ref<rowType>(),
seletctRow: ref<any>({}),
edit: (row?: rowType) => {
table.seletctRow = {
...(row || ({} as any)),
@ -176,12 +174,12 @@ const table = {
});
},
delete: (row: rowType) => {
remove_api(row.id as string).then(resp=>{
if(resp.status === 200) {
message.success('操作成功!')
table.refresh()
}else message.warning('操作失败!')
})
remove_api(row.id as string).then((resp) => {
if (resp.status === 200) {
message.success('操作成功!');
table.refresh();
} else message.warning('操作失败!');
});
},
refresh: () => {
tableRef.value && tableRef.value.reload();

View File

@ -1,19 +1,46 @@
<template>
<div style="width: 100%; height: 400px">
<el-amap
>
</el-amap>
<AmapComponent>
<el-amap-label-marker
v-for="i in point"
:key="i"
:position="i.geometry.coordinates"
:text="{
content: i.properties.deviceName,
direction: 'right',
style: {
fontSize: 15,
fillColor: '#fff',
strokeColor: 'rgba(255,0,0,0.5)',
strokeWidth: 2,
padding: [3, 10],
backgroundColor: 'yellow',
borderColor: '#ccc',
borderWidth: 3,
},
}"
:icon="{
image: 'https://a.amap.com/jsapi_demos/static/images/poi-marker.png',
anchor: 'bottom-center',
size: [25, 34],
clipOrigin: [459, 92],
clipSize: [50, 68],
}"
>123</el-amap-label-marker
>
</AmapComponent>
</div>
</template>
<script lang="ts" setup>
import { initAMapApiLoader } from '@vuemap/vue-amap';
import '@vuemap/vue-amap/dist/style.css';
initAMapApiLoader({
// key: '95fa72137f4263f8e64ae01f766ad09c',
key: 'a0415acfc35af15f10221bfa5a6850b4',
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0',
});
import AmapComponent from '@/components/AMapComponent/index.vue';
import { getGo } from '@/api/device/dashboard';
let point = ref();
const getMapData = async () => {
const res = await getGo({});
point.value = res.result?.features;
};
getMapData();
</script>
<style scoped>
</style>

View File

@ -28,8 +28,7 @@
:chartXData="barChartXData"
:chartYData="barChartYData"
></BarChart> -->
<Charts :options="onlineOptions"></Charts>
</TopCard
<Charts :options="onlineOptions"></Charts> </TopCard
></a-col>
<a-col :span="6"
><TopCard
@ -54,7 +53,7 @@
</template>
</Guide>
<div class="message-chart">
<Charts :options="devMegOptions"></Charts>
<Charts :options="devMegOptions"></Charts>
</div>
</div>
</a-col>
@ -74,7 +73,7 @@
</template>
<script lang="ts" setup>
import TimeSelect from './components/TimeSelect.vue';
import Charts from './components/Charts.vue'
import Charts from './components/Charts.vue';
import Guide from './components/Guide.vue';
import {
productCount,
@ -86,7 +85,8 @@ import encodeQuery from '@/utils/encodeQuery';
import { getImage } from '@/utils/comm';
import type { Footer } from '@/views/device/DashBoard/typings';
import TopCard from '@/views/device/DashBoard/components/TopCard.vue';
import Amap from './components/Amap.vue'
import { useMenuStore } from '@/store/menu';
import Amap from './components/Amap.vue';
let productTotal = ref(0);
let productFooter = ref<Footer[]>([
{
@ -133,6 +133,7 @@ let messageMaxChartYData = ref<number>();
let onlineOptions = ref<any>({});
let TodayDevOptions = ref<any>({});
let devMegOptions = ref<any>({});
const menuStore = useMenuStore();
const quickBtnList = [
{ label: '昨日', value: 'yesterday' },
{ label: '近一周', value: 'week' },
@ -140,54 +141,60 @@ const quickBtnList = [
{ label: '近一年', value: 'year' },
];
const getProductData = () => {
productCount().then((res) => {
if (res.status == 200) {
productTotal.value = res.result;
}
});
productCount({
terms: [
{
column: 'state',
value: '1',
},
],
}).then((res) => {
if (res.status == 200) {
productFooter.value[0].value = res.result;
}
});
productCount({
terms: [
{
column: 'state',
value: '0',
},
],
}).then((res) => {
if (res.status == 200) {
productFooter.value[1].value = res.result;
}
});
if (menuStore.hasMenu('device/Product')) {
productCount().then((res) => {
if (res.status == 200) {
productTotal.value = res.result;
}
});
productCount({
terms: [
{
column: 'state',
value: '1',
},
],
}).then((res) => {
if (res.status == 200) {
productFooter.value[0].value = res.result;
}
});
productCount({
terms: [
{
column: 'state',
value: '0',
},
],
}).then((res) => {
if (res.status == 200) {
productFooter.value[1].value = res.result;
}
});
}
};
getProductData();
const getDeviceData = () => {
deviceCount().then((res) => {
if (res.status == 200) {
deviceTotal.value = res.result;
}
});
deviceCount(encodeQuery({ terms: { state: 'online' } })).then((res) => {
if (res.status == 200) {
deviceFooter.value[0].value = res.result;
deviceOnline.value = res.result;
}
});
deviceCount(encodeQuery({ terms: { state: 'offline' } })).then((res) => {
if (res.status == 200) {
deviceFooter.value[1].value = res.result;
}
});
if (menuStore.hasMenu('device/Instance')) {
deviceCount().then((res) => {
if (res.status == 200) {
deviceTotal.value = res.result;
}
});
deviceCount(encodeQuery({ terms: { state: 'online' } })).then((res) => {
if (res.status == 200) {
deviceFooter.value[0].value = res.result;
deviceOnline.value = res.result;
}
});
deviceCount(encodeQuery({ terms: { state: 'offline' } })).then(
(res) => {
if (res.status == 200) {
deviceFooter.value[1].value = res.result;
}
},
);
}
};
getDeviceData();
const getOnline = () => {
@ -213,163 +220,167 @@ const getOnline = () => {
.reverse();
const y = res.result.map((item: any) => item.data.value);
const onlineYdata = y;
onlineYdata.reverse()
setOnlineChartOpition(x,onlineYdata);
onlineYdata.reverse();
setOnlineChartOpition(x, onlineYdata);
deviceFooter.value[0].value = y?.[1];
}
});
};
const setOnlineChartOpition = (x:Array<any>,y:Array<number>):void=>{
const setOnlineChartOpition = (x: Array<any>, y: Array<number>): void => {
onlineOptions.value = {
xAxis: {
type: 'category',
data: x,
show: false,
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '5%',
bottom: 0,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
series: [
{
name: '在线数',
data: y,
type: 'bar',
showBackground: true,
itemStyle: {
color: '#D3ADF7',
},
},
],
};
}
const setTodayDevChartOption = (x:Array<any>,y:Array<number>):void =>{
TodayDevOptions = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
boundaryGap: false,
show: false,
data:x
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '2%',
bottom: 0,
},
series: [
{
name: '消息量',
data: y,
type: 'line',
smooth: true, // 线
symbolSize: 0, //
color: '#F29B55',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#FBBB87', // 100%
},
{
offset: 1,
color: '#FFFFFF', // 0%
},
],
global: false, // false
},
},
},
],
};
}
const setDevMesChartOption = (x:Array<any>,y:Array<number>,maxY:number):void =>{
devMegOptions.value = {
xAxis: {
type: 'category',
boundaryGap: false,
data: x,
type: 'category',
data: x,
show: false,
},
yAxis: {
type: 'value',
},
tooltip: {
trigger: 'axis',
formatter: '{b0}<br />{a0}: {c0}',
// formatter: '{b0}<br />{a0}: {c0}<br />{a1}: {c1}%'
type: 'value',
show: false,
},
grid: {
top: '2%',
bottom: '5%',
left: maxY > 100000 ? '90px' : '50px',
right: '50px',
top: '5%',
bottom: 0,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
series: [
{
name: '消息量',
data: y,
type: 'bar',
// type: 'line',
// smooth: true,
color: '#597EF7',
barWidth: '30%',
// areaStyle: {
// color: {
// type: 'linear',
// x: 0,
// y: 0,
// x2: 0,
// y2: 1,
// colorStops: [
// {
// offset: 0,
// color: '#685DEB', // 100%
// },
// {
// offset: 1,
// color: '#FFFFFF', // 0%
// },
// ],
// global: false, // false
// },
// },
},
{
name: '占比',
data: y,
// data: percentageY,
type: 'line',
smooth: true,
symbolSize: 0, //
color: '#96ECE3',
},
{
name: '在线数',
data: y,
type: 'bar',
showBackground: true,
itemStyle: {
color: '#D3ADF7',
},
},
],
}
}
};
};
const setTodayDevChartOption = (x: Array<any>, y: Array<number>): void => {
TodayDevOptions = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
boundaryGap: false,
show: false,
data: x,
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '2%',
bottom: 0,
},
series: [
{
name: '消息量',
data: y,
type: 'line',
smooth: true, // 线
symbolSize: 0, //
color: '#F29B55',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#FBBB87', // 100%
},
{
offset: 1,
color: '#FFFFFF', // 0%
},
],
global: false, // false
},
},
},
],
};
};
const setDevMesChartOption = (
x: Array<any>,
y: Array<number>,
maxY: number,
): void => {
devMegOptions.value = {
xAxis: {
type: 'category',
boundaryGap: false,
data: x,
},
yAxis: {
type: 'value',
},
tooltip: {
trigger: 'axis',
formatter: '{b0}<br />{a0}: {c0}',
// formatter: '{b0}<br />{a0}: {c0}<br />{a1}: {c1}%'
},
grid: {
top: '2%',
bottom: '5%',
left: maxY > 100000 ? '90px' : '50px',
right: '50px',
},
series: [
{
name: '消息量',
data: y,
type: 'bar',
// type: 'line',
// smooth: true,
color: '#597EF7',
barWidth: '30%',
// areaStyle: {
// color: {
// type: 'linear',
// x: 0,
// y: 0,
// x2: 0,
// y2: 1,
// colorStops: [
// {
// offset: 0,
// color: '#685DEB', // 100%
// },
// {
// offset: 1,
// color: '#FFFFFF', // 0%
// },
// ],
// global: false, // false
// },
// },
},
{
name: '占比',
data: y,
// data: percentageY,
type: 'line',
smooth: true,
symbolSize: 0, //
color: '#96ECE3',
},
],
};
};
getOnline();
//
const getDevice = () => {
@ -427,7 +438,7 @@ const getDevice = () => {
);
const x = today.map((item: any) => item.data.timeString).reverse();
const y = today.map((item: any) => item.data.value).reverse();
setTodayDevChartOption(x,y);
setTodayDevChartOption(x, y);
}
});
};
@ -452,7 +463,7 @@ const getEcharts = (data: any) => {
_time = '1M';
format = 'yyyy年-M月';
}
dashboard([
{
dashboard: 'device',
@ -468,7 +479,7 @@ const getEcharts = (data: any) => {
to: data.end,
},
},
]).then((res:any) => {
]).then((res: any) => {
if (res.status === 200) {
const x = res.result
.map((item: any) =>
@ -478,23 +489,27 @@ const getEcharts = (data: any) => {
)
.reverse();
const y = res.result.map((item: any) => item.data.value).reverse();
const maxY = Math.max.apply(null, messageChartYData.value.length ? messageChartYData.value : [0]);
setDevMesChartOption(x,y,maxY);
const maxY = Math.max.apply(
null,
messageChartYData.value.length ? messageChartYData.value : [0],
);
setDevMesChartOption(x, y, maxY);
}
});
};
</script>
<style lang="less" scoped>
.message-card,.device-position{
.message-card,
.device-position {
margin-top: 24px;
padding: 24px;
background-color: white;
}
.message-chart {
width: 100%;
height: 400px;
height: 400px;
}
.amap-box{
.amap-box {
height: 500px;
width: 100%;
}

View File

@ -18,7 +18,7 @@
@search="handleSearch"
type="simple"
/>
<JTable
<JProTable
ref="bindDeviceRef"
:columns="columns"
:request="query"
@ -78,7 +78,7 @@
:status="statusMap.get(slotProps.state.value)"
/>
</template>
</JTable>
</JProTable>
</div>
</a-modal>
</template>

View File

@ -0,0 +1,117 @@
<template>
<a-select allowClear v-model:value="_value" @change="onChange" placeholder="请选择" style="width: 100%">
<a-select-option
v-for="item in list"
:key="item.id"
:value="item.id"
:label="item.name"
:filter-option="filterOption"
>{{ item.name }}</a-select-option
>
</a-select>
</template>
<script lang="ts" setup>
import {
edgeCollector,
edgePoint,
} from '@/api/device/instance';
const _props = defineProps({
modelValue: {
type: String,
default: undefined,
},
type: {
type: String,
default: 'POINT',
},
id: {
type: String,
default: '',
},
edgeId: {
type: String,
default: '',
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
type Emits = {
(e: 'update:modelValue', data: string | undefined): void;
};
const emit = defineEmits<Emits>();
const list = ref<any[]>([]);
const _value = ref<string | undefined>(undefined);
watchEffect(() => {
_value.value = _props.modelValue;
});
const onChange = (_val: string) => {
emit('update:modelValue', _val);
};
const getCollector = async (_val: string) => {
if (!_val) {
return [];
} else {
const resp = await edgeCollector(_props.edgeId, {
terms: [
{
terms: [
{
column: 'channelId',
value: _val,
},
],
},
],
});
if (resp.status === 200) {
list.value = (resp.result as any[])?.[0] || []
}
}
};
const getPoint = async (_val: string) => {
if (!_val) {
return [];
} else {
const resp = await edgePoint(_props.edgeId, {
terms: [
{
terms: [
{
column: 'collectorId',
value: _val,
},
],
},
],
});
if (resp.status === 200) {
list.value = (resp.result as any[])?.[0] || []
}
}
};
watchEffect(() => {
if (_props.id) {
if (_props.type === 'POINT') {
getPoint(_props.id);
} else {
getCollector(_props.id);
}
} else {
list.value = [];
}
});
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,212 @@
<template>
<a-modal
width="900px"
title="批量映射"
visible
@ok="handleClick"
@cancel="handleClose"
>
<div class="map-tree">
<div class="map-tree-top">
采集器的点位名称与属性名称一致时将自动映射绑定有多个采集器点位名称与属性名称一致时以第1个采集器的点位数据进行绑定
</div>
<a-spin :spinning="loading">
<div class="map-tree-content">
<a-card class="map-tree-content-card" title="源数据">
<a-tree
checkable
:height="300"
:tree-data="dataSource"
:checkedKeys="checkedKeys"
@check="onCheck"
/>
</a-card>
<div style="width: 100px">
<a-button
:disabled="rightList.length >= leftList.length"
@click="onRight"
>加入右侧</a-button
>
</div>
<a-card class="map-tree-content-card" title="采集器">
<a-list
size="small"
:data-source="rightList"
class="map-tree-content-card-list"
>
<template #renderItem="{ item }">
<a-list-item>
{{ item.title }}
<template #actions>
<a-popconfirm
title="确定删除?"
@confirm="_delete(item.key)"
>
<AIcon type="DeleteOutlined" />
</a-popconfirm>
</template>
</a-list-item>
</template>
</a-list>
</a-card>
</div>
</a-spin>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { treeEdgeMap, saveEdgeMap, addDevice } from '@/api/device/instance';
import { message } from 'ant-design-vue/es';
const _props = defineProps({
metaData: {
type: Array,
default: () => [],
},
deviceId: {
type: String,
default: '',
},
edgeId: {
type: String,
default: '',
},
deviceData: {
type: Object,
},
});
const _emits = defineEmits(['close', 'save']);
const checkedKeys = ref<string[]>([]);
const leftList = ref<any[]>([]);
const rightList = ref<any[]>([]);
const dataSource = ref<any[]>([]);
const loading = ref<boolean>(false);
const handleData = (data: any[], type: string) => {
data.forEach((item) => {
item.key = item.id;
item.title = item.name;
item.checkable = type === 'collectors';
if (
item.collectors &&
Array.isArray(item.collectors) &&
item.collectors.length
) {
item.children = handleData(item.collectors, 'collectors');
}
if (item.points && Array.isArray(item.points) && item.points.length) {
item.children = handleData(item.points, 'points');
}
});
return data as any[];
};
const handleSearch = async () => {
loading.value = true;
const resp = await treeEdgeMap(_props.edgeId);
loading.value = false;
if (resp.status === 200) {
dataSource.value = handleData((resp.result as any[])?.[0], 'channel');
}
};
const onCheck = (keys: string[], e: any) => {
checkedKeys.value = [...keys];
leftList.value = e?.checkedNodes || [];
};
const onRight = () => {
rightList.value = leftList.value;
};
const _delete = (_key: string) => {
const _index = rightList.value.findIndex((i) => i.key === _key);
rightList.value.splice(_index, 1);
checkedKeys.value = rightList.value.map((i) => i.key);
leftList.value = rightList.value;
};
const handleClick = async () => {
if (!rightList.value.length) {
message.warning('请选择采集器');
} else {
const params: any[] = [];
rightList.value.map((item: any) => {
const array = (item.children || []).map((element: any) => ({
channelId: item.parentId,
collectorId: element.collectorId,
pointId: element.id,
metadataType: 'property',
metadataId: (_props.metaData as any[]).find(
(i: any) => i.name === element.name,
)?.metadataId,
provider: dataSource.value.find(
(it: any) => it.id === item.parentId,
).provider,
}));
params.push(...array);
});
const filterParms = params.filter((item) => !!item.metadataId);
if (_props.deviceId) {
if (filterParms && filterParms.length !== 0) {
const res = await saveEdgeMap(_props.edgeId, {
deviceId: _props.deviceId,
provider: filterParms[0]?.provider,
requestList: filterParms,
});
if (res.status === 200) {
message.success('操作成功');
_emits('save');
}
} else {
message.error('暂无对应属性的映射');
}
} else {
if (filterParms && filterParms.length !== 0) {
const res = await addDevice(_props.deviceData);
if (res.status === 200) {
const resq = await saveEdgeMap(_props.edgeId, {
deviceId: res.result?.id,
provider: filterParms[0]?.provider,
requestList: filterParms,
});
if (res.status === 200) {
message.success('操作成功');
_emits('save');
}
}
}
}
}
};
const handleClose = () => {
_emits('close');
};
onMounted(() => {
if (_props.edgeId) {
handleSearch();
}
});
</script>
<style lang="less" scoped>
.map-tree-content {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
.map-tree-content-card {
width: 350px;
height: 400px;
.map-tree-content-card-list {
overflow-y: auto;
height: 300px;
}
}
}
</style>

View File

@ -0,0 +1,337 @@
<template>
<a-spin :spinning="loading" v-if="_metadata">
<a-card :bordered="false">
<template #title>
<TitleComponent data="点位映射"></TitleComponent>
</template>
<template #extra>
<a-space>
<a-button @click="showModal">批量映射</a-button>
<a-button type="primary" @click="onSave">保存</a-button>
</a-space>
</template>
<a-form ref="formRef" :model="modelRef">
<a-table :dataSource="modelRef.dataSource" :columns="columns">
<template #headerCell="{ column }">
<template v-if="column.key === 'collectorId'">
采集器
<a-tooltip title="边缘网关代理的真实物理设备">
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</template>
</template>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'channelId'">
<a-form-item
:name="['dataSource', index, 'channelId']"
>
<a-select
style="width: 100%"
v-model:value="record[column.dataIndex]"
placeholder="请选择"
allowClear
:filter-option="filterOption"
>
<a-select-option
v-for="item in channelList"
:key="item.value"
:value="item.value"
:label="item.label"
>{{ item.label }}</a-select-option
>
</a-select>
</a-form-item>
</template>
<template v-if="column.dataIndex === 'collectorId'">
<a-form-item
:name="['dataSource', index, 'collectorId']"
:rules="[
{
required: !!record.channelId,
message: '请选择采集器',
},
]"
>
<MSelect
v-model="record[column.dataIndex]"
:id="record.channelId"
type="COLLECTOR"
:edgeId="instanceStore.current.id"
/>
</a-form-item>
</template>
<template v-if="column.dataIndex === 'pointId'">
<a-form-item
:name="['dataSource', index, 'pointId']"
:rules="[
{
required: !!record.channelId,
message: '请选择点位',
},
]"
>
<MSelect
v-model="record[column.dataIndex]"
:id="record.collectorId"
type="POINT"
:edgeId="instanceStore.current.id"
/>
</a-form-item>
</template>
<template v-if="column.dataIndex === 'id'">
<a-badge
v-if="record[column.dataIndex]"
status="success"
text="已绑定"
/>
<a-badge v-else status="error" text="未绑定" />
</template>
<template v-if="column.key === 'action'">
<a-tooltip title="解绑">
<a-popconfirm
title="确认解绑"
:disabled="!record.id"
@confirm="unbind(record.id)"
>
<a-button type="link" :disabled="!record.id"
><AIcon type="icon-jiebang"
/></a-button>
</a-popconfirm>
</a-tooltip>
</template>
</template>
</a-table>
</a-form>
</a-card>
<PatchMapping
:deviceId="instanceStore.current.parentId"
v-if="visible"
@close="visible = false"
@save="onPatchBind"
:metaData="modelRef.dataSource"
:edgeId="instanceStore.current.id"
:deviceData="deviceData"
/>
</a-spin>
<a-card v-else>
<JEmpty description="暂无数据,请配置物模型" style="margin: 10% 0" />
</a-card>
</template>
<script lang="ts" setup>
import { useInstanceStore } from '@/store/instance';
import {
getEdgeMap,
saveEdgeMap,
removeEdgeMap,
edgeChannel,
addDevice,
editDevice,
} from '@/api/device/instance';
import MSelect from './MSelect.vue';
import PatchMapping from './PatchMapping.vue';
import { message } from 'ant-design-vue/es';
import { inject } from 'vue';
const columns = [
{
title: '名称',
dataIndex: 'metadataName',
key: 'metadataName',
width: '20%',
},
{
title: '通道',
dataIndex: 'channelId',
key: 'channelId',
width: '20%',
},
{
title: '采集器',
dataIndex: 'collectorId',
key: 'collectorId',
width: '20%',
},
{
title: '点位',
key: 'pointId',
dataIndex: 'pointId',
width: '20%',
},
{
title: '状态',
key: 'id',
dataIndex: 'id',
width: '10%',
},
{
title: '操作',
key: 'action',
width: '10%',
},
];
const validate = inject('validate');
const form = ref();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const props = defineProps(['productList']);
const _emit = defineEmits(['close']);
const instanceStore = useInstanceStore();
let _metadata = ref();
const loading = ref<boolean>(false);
const channelList = ref([]);
const modelRef = reactive({
dataSource: [],
});
const deviceData = ref();
const formRef = ref();
const visible = ref<boolean>(false);
const getChannel = async () => {
if (instanceStore.current?.id) {
const resp: any = await edgeChannel(instanceStore.current.id);
if (resp.status === 200) {
channelList.value = resp.result?.[0]?.map((item: any) => ({
label: item.name,
value: item.id,
provider: item.provider,
}));
}
}
};
const handleSearch = async () => {
loading.value = true;
modelRef.dataSource = _metadata;
getChannel();
if (_metadata && _metadata.length) {
const resp: any = await getEdgeMap(instanceStore.current?.orgId || '', {
deviceId: instanceStore.current.id,
query: {},
}).catch(() => {
modelRef.dataSource = _metadata;
loading.value = false;
});
if (resp.status === 200) {
const array = resp.result?.[0].reduce((x: any, y: any) => {
const metadataId = _metadata.find(
(item: any) => item.metadataId === y.metadataId,
);
if (metadataId) {
Object.assign(metadataId, y);
} else {
x.push(y);
}
return x;
}, _metadata);
modelRef.dataSource = array;
}
}
loading.value = false;
};
const unbind = async (id: string) => {
if (id) {
const resp = await removeEdgeMap(
instanceStore.current?.parentId || '',
{
deviceId: instanceStore.current.id,
idList: [id],
},
);
if (resp.status === 200) {
message.success('操作成功!');
handleSearch();
}
}
};
const onPatchBind = () => {
visible.value = false;
_emit('close');
};
onMounted(() => {
handleSearch();
});
watchEffect(() => {
if (instanceStore.current?.metadata) {
_metadata.value = instanceStore.current?.metadata;
} else {
_metadata.value = {};
}
});
const onSave = async () => {
form.value = await validate();
if (form.value) {
formRef.value.validateFields().then(async () => {
if (modelRef.dataSource.length === 0) {
message.error('请配置物模型');
} else {
channelList.value.forEach((item: any) => {
modelRef.dataSource.forEach((i: any) => {
if (item.value === i.channelId) {
i.provider = item.provider;
}
});
});
const formData = {
...form.value,
productName: props.productList.find(
(item: any) => item.id === form.value?.productId,
).name,
parentId: instanceStore.current.id,
id: instanceStore.current.parentId
? instanceStore.current.parentId
: undefined,
};
const resq = instanceStore.current.parentId
? await editDevice(formData)
: await addDevice(formData);
if (resq.status === 200) {
const array = modelRef.dataSource.filter(
(item: any) => item.channelId,
);
const submitData = {
deviceId: instanceStore.current.parentId
? instanceStore.current.parentId
: resq.result?.id,
provider: array?.[0]?.provider,
requestList: array,
};
save(submitData);
}
}
});
}
};
const save = async (item: any) => {
const res = await saveEdgeMap(instanceStore.current.id, item);
if (res.status === 200) {
message.success('保存成功');
_emit('close');
}
};
const showModal = async () => {
form.value = await validate();
if (form.value) {
const formData = {
...form.value,
productName: props.productList.find(
(item: any) => item.id === form.value?.productId,
).name,
parentId: instanceStore.current.id,
};
deviceData.value = formData;
}
visible.value = true;
};
</script>
<style lang="less" scoped>
:deep(.ant-form-item) {
margin: 0 !important;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div>
<TitleComponent data="基本信息">
<template #extra>
<j-button @click="comeBack">返回</j-button>
</template>
</TitleComponent>
<j-form layout="vertical" :model="form" ref="formRef">
<j-row :gutter="24">
<j-col :span="12">
<j-form-item
label="设备名称"
name="name"
:rules="{ required: true, message: '请输入设备名称' }"
>
<j-input v-model:value="form.name"></j-input>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="产品名称"
name="productId"
:rules="{ required: true, message: '请选择产品名称' }"
>
<j-select
:disabled="props.childData?.id"
@change="selectChange"
v-model:value="form.productId"
>
<j-select-option
v-for="i in productList"
:key="i.id"
:value="i.id"
>{{ i.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
</j-row>
<j-row :gutter="24" v-if="visible">
<j-col :span="24"
><EdgeMap :productList="productList" @close="comeBack"
/></j-col>
</j-row>
</j-form>
</div>
</template>
<script lang="ts" setup>
import { getProductListNoPage } from '@/api/device/instance';
import EdgeMap from '../EdgeMap/index.vue';
import { useInstanceStore } from '@/store/instance';
import { storeToRefs } from 'pinia';
import { provide } from 'vue';
import { getEdgeMap, removeEdgeMap } from '@/api/device/instance';
const instanceStore = useInstanceStore();
const { current } = storeToRefs(instanceStore);
const props = defineProps(['childData']);
const form = reactive({
name: '',
productId: '',
});
const formRef = ref();
const emit = defineEmits(['closeChildSave']);
const productList = ref();
const visible = ref(false);
const getProductList = async () => {
const res = await getProductListNoPage({
terms: [{ column: 'accessProvider', value: 'edge-child-device' }],
});
if (res.status === 200) {
productList.value = res.result;
}
};
getProductList();
const selectChange = (e: any) => {
if (e) {
visible.value = true;
}
const item = productList.value.filter((i: any) => i.id === e)[0];
const array = JSON.parse(item.metadata || [])?.properties?.map(
(i: any) => ({
metadataType: 'property',
metadataName: `${i.name}(${i.id})`,
metadataId: i.id,
name: i.name,
}),
);
current.value.metadata = array;
};
watchEffect(() => {
if (props.childData?.id) {
current.value.parentId = props.childData.id;
form.name = props.childData?.name;
form.productId = props.childData?.productId;
if (props.childData.deriveMetadata) {
const metadata = JSON.parse(
props.childData?.deriveMetadata || {},
)?.properties?.map((item: any) => ({
metadataId: item.id,
metadataName: `${item.name}(${item.id})`,
metadataType: 'property',
name: item.name,
}));
if (metadata && metadata.length !== 0) {
getEdgeMap(current.value.id, {
deviceId: props.childData.id,
query: {},
}).then((res) => {
if (res.status === 200) {
// console.log(res.result)
//
const array = res.result[0]?.reduce(
(x: any, y: any) => {
const metadataId = metadata.find(
(item: any) =>
item.metadataId === y.metadataId,
);
if (metadataId) {
Object.assign(metadataId, y);
} else {
x.push(y);
}
return x;
},
metadata,
);
//
const items = array.filter(
(item: any) => item.metadataName,
);
current.value.metadata = items;
const delList = array
.filter((a: any) => !a.metadataName)
.map((b: any) => b.id);
//
if (delList && delList.length !== 0) {
removeEdgeMap(current.value.id, {
deviceId: props.childData.id,
idList: [...delList],
});
}
}
});
}
}
visible.value = true;
}
});
const validate = async () => {
return formRef.value.validateFields();
};
provide('validate', validate);
const comeBack = () => {
emit('closeChildSave');
};
</script>
<style lang="less" scoped>
</style>

View File

@ -1,91 +1,113 @@
<template>
<a-card>
<Search
:columns="columns"
target="child-device"
@search="handleSearch"
class="child-device-search"
<SaveChild
v-if="childVisible"
@close-child-save="closeChildSave"
:childData="current"
/>
<JTable
ref="childDeviceRef"
:columns="columns"
:request="query"
:defaultParams="{
terms: [
{
column: 'parentId',
value: detail?.id || '',
termType: 'eq',
},
],
}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
:model="'TABLE'"
>
<template #headerTitle>
<a-space>
<a-button type="primary"> 新增并绑定 </a-button>
<a-button type="primary" @click="visible = true">
绑定
</a-button>
<a-popconfirm title="确认解绑吗?" @confirm="handleUnBind">
<a-button type="primary"> 批量解绑 </a-button>
</a-popconfirm>
</a-space>
</template>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? moment(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
<div v-else>
<Search
:columns="columns"
target="child-device"
@search="handleSearch"
class="child-device-search"
/>
<JProTable
ref="childDeviceRef"
:columns="columns"
:request="query"
:defaultParams="{
terms: [
{
column: 'parentId',
value: detail?.id || '',
termType: 'eq',
},
],
}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
:model="'TABLE'"
>
<template #headerTitle>
<j-space>
<PermissionButton
type="primary"
v-if="
detail?.accessProvider ===
'official-edge-gateway'
"
hasPermission="device/Instance:update"
@click="
current = {};
childVisible = true;
"
>新增并绑定</PermissionButton
>
<a-button
<PermissionButton
type="primary"
@click="visible = true"
hasPermission="device/Instance:update"
>
绑定</PermissionButton
>
<PermissionButton
type="primary"
hasPermission="device/Instance:update"
:popConfirm="{
title: '确定解绑吗?',
onConfirm: handleUnBind,
}"
>批量解除</PermissionButton
>
</j-space>
</template>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? moment(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
<template #action="slotProps">
<j-space :size="16">
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
style="padding: 0"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<BindChildDevice v-if="visible" @change="closeBindDevice" />
style="padding: 0px"
:hasPermission="'device/Instance:' + i.key"
>
<template #icon
><AIcon :type="i.icon"
/></template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
<BindChildDevice v-if="visible" @change="closeBindDevice" />
</div>
</a-card>
</template>
@ -97,11 +119,17 @@ import { useInstanceStore } from '@/store/instance';
import { storeToRefs } from 'pinia';
import { message } from 'ant-design-vue';
import BindChildDevice from './BindChildDevice/index.vue';
import { usePermissionStore } from '@/store/permission';
import SaveChild from './SaveChild/index.vue';
const instanceStore = useInstanceStore();
const { detail } = storeToRefs(instanceStore);
const { detail } = storeToRefs(instanceStore);
const router = useRouter();
const childVisible = ref(false);
const permissionStore = usePermissionStore();
// watchEffect(() => {
// console.log(detail.value);
// });
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
@ -111,6 +139,7 @@ const childDeviceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const visible = ref<boolean>(false);
const current = ref({});
const columns = [
{
@ -192,7 +221,7 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
},
},
{
key: 'unbind',
key: 'action',
text: '解绑',
tooltip: {
title: '解绑',
@ -215,6 +244,18 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
},
},
},
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
current.value = data;
childVisible.value = true;
},
},
];
};
@ -252,6 +293,10 @@ const closeBindDevice = (val: boolean) => {
childDeviceRef.value?.reload();
}
};
const closeChildSave = () => {
childVisible.value = false;
};
</script>
<style scoped lang="less">

View File

@ -1693,7 +1693,7 @@ const Status = defineComponent({
/>
)}
{
bindParentVisible && (
bindParentVisible.value && (
<BindParentDevice
data={device.value}
onCancel={() => {

View File

@ -1,13 +1,13 @@
<template>
<div class="wrapper">
<a-tabs v-model="activeKey" tab-position="left">
<a-tab-pane
<j-tabs v-model="activeKey" tab-position="left">
<j-tab-pane
v-for="func in newFunctions"
:key="func.id"
:tab="func.name"
>
<a-row :gutter="30">
<a-col :span="15">
<j-row :gutter="30">
<j-col :span="15">
<MonacoEditor
:ref="`monacoEditor${func.id}`"
v-model="func.json"
@ -15,31 +15,31 @@
style="height: 400px"
/>
<div class="editor-btn">
<a-space>
<a-button
<j-space>
<j-button
type="primary"
@click="handleExecute(func)"
>
执行
</a-button>
<a-button
</j-button>
<j-button
type="default"
@click="handleClear(func)"
>
清空
</a-button>
</a-space>
</j-button>
</j-space>
</div>
</a-col>
<a-col :span="9">
</j-col>
<j-col :span="9">
<h6>执行结果</h6>
<span class="execute-result">
{{ func.executeResult }}
</span>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</j-col>
</j-row>
</j-tab-pane>
</j-tabs>
</div>
</template>

View File

@ -1,21 +1,21 @@
<template>
<div class="wrapper">
<div class="tips">
<a-space>
<j-space>
<AIcon type="QuestionCircleOutlined" />
<span>精简模式下参数只支持输入框的方式录入</span>
</a-space>
</j-space>
</div>
<a-tabs v-model="activeKey" tab-position="left">
<a-tab-pane
<j-tabs v-model="activeKey" tab-position="left">
<j-tab-pane
v-for="func in newFunctions"
:key="func.id"
:tab="func.name"
>
<a-row :gutter="30">
<a-col :span="15">
<a-form :ref="`${func.id}Ref`" :model="func">
<a-table
<j-row :gutter="30">
<j-col :span="15">
<j-form :ref="`${func.id}Ref`" :model="func">
<j-table
:columns="columns"
:data-source="func.table"
:pagination="false"
@ -26,7 +26,7 @@
v-if="column.dataIndex === 'type'"
>
<span>{{ record.type }}</span>
<a-tooltip
<j-tooltip
v-if="record.type === 'object'"
>
<template slot="title">
@ -40,12 +40,12 @@
cursor: 'help',
}"
/>
</a-tooltip>
</j-tooltip>
</template>
<template
v-if="column.dataIndex === 'value'"
>
<a-form-item
<j-form-item
:name="['table', index, 'value']"
:rules="{
required: true,
@ -82,37 +82,37 @@
: undefined
"
/>
</a-form-item>
</j-form-item>
</template>
</template>
</a-table>
</a-form>
</j-table>
</j-form>
<div class="editor-btn">
<a-space>
<a-button
<j-space>
<j-button
type="primary"
@click="handleExecute(func)"
>
执行
</a-button>
<a-button
</j-button>
<j-button
type="default"
@click="handleClear(func)"
>
清空
</a-button>
</a-space>
</j-button>
</j-space>
</div>
</a-col>
<a-col :span="9">
</j-col>
<j-col :span="9">
<h6>执行结果</h6>
<span class="execute-result">
{{ func.executeResult }}
</span>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</j-col>
</j-row>
</j-tab-pane>
</j-tabs>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<a-card>
<a-empty
<j-card>
<j-empty
v-if="!metadata || (metadata && !metadata.functions.length)"
style="margin-top: 50px"
>
@ -9,15 +9,15 @@
<!-- <a @click="emits('onJump', 'Metadata')">物模型属性功能</a> -->
<a @click="onJump">物模型属性功能</a>
</template>
</a-empty>
</j-empty>
<template v-else>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="Simple" tab="精简模式" />
<a-tab-pane key="Advance" tab="高级模式" />
</a-tabs>
<j-tabs v-model:activeKey="activeKey">
<j-tab-pane key="Simple" tab="精简模式" />
<j-tab-pane key="Advance" tab="高级模式" />
</j-tabs>
<component :is="tabs[activeKey]" />
</template>
</a-card>
</j-card>
</template>
<script setup lang="ts">

View File

@ -25,13 +25,13 @@
instanceStore.current.deviceType?.text
}}</j-descriptions-item>
<j-descriptions-item label="固件版本">{{
instanceStore.current.firmwareInfo?.version
instanceStore.current?.firmwareInfo?.version
}}</j-descriptions-item>
<j-descriptions-item label="连接协议">{{
instanceStore.current?.protocolName
instanceStore.current?.transport
}}</j-descriptions-item>
<j-descriptions-item label="消息协议">{{
instanceStore.current.transport
instanceStore.current.protocolName
}}</j-descriptions-item>
<j-descriptions-item label="创建时间">{{
instanceStore.current.createTime

View File

@ -109,7 +109,9 @@ const loading = ref<boolean>(false);
const instanceStore = useInstanceStore();
const formRef = ref();
const modelRef = reactive({
const modelRef = reactive<{
metrics: any[]
}>({
metrics: [],
});

View File

@ -23,11 +23,12 @@
:tab="i.tab"
/>
</j-tabs>
<JEmpty v-else style="margin: 250px 0" />
<JEmpty v-else style="margin: 180px 0" />
</div>
<div class="property-box-right">
<Event v-if="type === 'event'" :data="data" />
<Property v-else :data="properties" />
<Property v-else-if="type === 'property'" :data="properties" />
<JEmpty v-else style="margin: 220px 0" />
</div>
</div>
</j-card>
@ -97,6 +98,13 @@ const onSearch = () => {
} else {
tabList.value = _.cloneDeep(arr)
}
const dt = tabList.value?.[0]
if (dt) {
data.value = dt
type.value = dt.type;
} else {
type.value = ''
}
};
const tabChange = (key: string) => {
const dt = tabList.value.find((i) => i.key === key);

View File

@ -9,7 +9,9 @@
<div>
<div style="display: flex; align-items: center">
<AIcon type="ArrowLeftOutlined" @click="onBack" />
<div style="margin-left: 20px">{{ instanceStore.current.name }}</div>
<div style="margin-left: 20px">
{{ instanceStore.current.name }}
</div>
<j-divider type="vertical" />
<j-space>
<j-badge
@ -79,7 +81,7 @@
<j-descriptions-item label="所属产品">
<PermissionButton
type="link"
style="margin-top: -5px; padding: 0"
style="margin-top: -5px; padding: 0"
@click="jumpProduct"
hasPermission="device/Product:view"
>
@ -116,8 +118,8 @@ import Function from './Function/index.vue';
import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Parsing from './Parsing/index.vue'
import Log from './Log/index.vue'
import Parsing from './Parsing/index.vue';
import Log from './Log/index.vue';
import { _deploy, _disconnect } from '@/api/device/instance';
import { message } from 'jetlinks-ui-components';
import { getImage } from '@/utils/comm';
@ -149,17 +151,13 @@ const list = ref([
key: 'Metadata',
tab: '物模型',
},
{
key: 'Log',
tab: '日志管理',
},
{
key: 'Function',
tab: '设备功能',
},
{
key: 'ChildDevice',
tab: '子设备',
key: 'Log',
tab: '日志管理',
},
]);
@ -174,7 +172,7 @@ const tabs = {
OPCUA,
EdgeMap,
Parsing,
Log
Log,
};
const getStatus = (id: string) => {
@ -255,6 +253,17 @@ watchEffect(() => {
tab: '设备诊断',
});
}
if (
instanceStore.current.features?.find(
(item: any) => item.id === 'transparentCodec',
) &&
!keys.includes('Parsing')
) {
list.value.push({
key: 'Parsing',
tab: '数据解析',
});
}
if (
instanceStore.current.protocol === 'modbus-tcp' &&
!keys.includes('Modbus')
@ -273,6 +282,13 @@ watchEffect(() => {
tab: 'OPC UA',
});
}
if (instanceStore.current.deviceType?.value === 'gateway') {
//
list.value.push({
key: 'ChildDevice',
tab: '子设备',
});
}
if (
instanceStore.current.accessProvider === 'edge-child-device' &&
instanceStore.current.parentId &&
@ -283,15 +299,6 @@ watchEffect(() => {
tab: '边缘端映射',
});
}
if (
instanceStore.current.features?.find((item: any) => item.id === 'transparentCodec') &&
!keys.includes('Parsing')
) {
list.value.push({
key: 'Parsing',
tab: '数据解析',
});
}
});
onUnmounted(() => {

View File

@ -51,7 +51,6 @@
import { queryNoPagingPost } from '@/api/device/product';
import { downloadFile } from '@/utils/utils';
import encodeQuery from '@/utils/encodeQuery';
import { BASE_API_PATH } from '@/utils/variable';
import { deviceExport } from '@/api/device/instance';
const emit = defineEmits(['close']);

View File

@ -1,92 +1,106 @@
<template>
<j-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleCancel" @cancel="handleCancel">
<j-modal
:maskClosable="false"
width="800px"
:visible="true"
title="当前进度"
@ok="handleCancel"
@cancel="handleCancel"
>
<div>
<j-badge v-if="flag" status="processing" text="进行中" />
<j-badge v-else status="success" text="已完成" />
<j-badge v-else status="success" text="已完成" />
</div>
<p>总数量{{count}}</p>
<a style="color: red">{{errMessage}}</a>
<p>总数量{{ count }}</p>
<a style="color: red">{{ errMessage }}</a>
</j-modal>
</template>
<script lang="ts" setup>
import { EventSourcePolyfill } from 'event-source-polyfill'
import { EventSourcePolyfill } from 'event-source-polyfill';
const emit = defineEmits(['close'])
const emit = defineEmits(['close', 'save']);
const props = defineProps({
api: {
type: String,
default: ''
default: '',
},
type: {
type: String,
default: ''
}
})
const eventSource = ref<Record<string, any>>({})
const count = ref<number>(0)
const flag = ref<boolean>(false)
const errMessage = ref<string>('')
const isSource = ref<boolean>(false)
const id = ref<string>('')
const source = ref<Record<string, any>>({})
default: '',
},
});
// const eventSource = ref<Record<string, any>>({})
const count = ref<number>(0);
const flag = ref<boolean>(false);
const errMessage = ref<string>('');
const isSource = ref<boolean>(false);
const id = ref<string>('');
const source = ref<Record<string, any>>({});
const handleCancel = () => {
emit('close')
}
emit('close');
emit('save');
};
// const handleOk = () => {
// emit('close');
// emit('save');
// };
const getData = (api: string) => {
let dt = 0
const _source = new EventSourcePolyfill(api)
source.value = _source
let dt = 0;
const _source = new EventSourcePolyfill(api);
source.value = _source;
_source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
switch (props.type) {
case 'active':
if (res.success) {
dt += res.total;
count.value = dt
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg
id.value = res.source.id
isSource.value = true
} else {
errMessage.value = res.message
}
}
break;
case 'sync':
dt += res;
count.value = dt
break;
case 'import':
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt
} else {
errMessage.value = res.message
}
break;
default:
break;
}
const res = JSON.parse(e.data);
switch (props.type) {
case 'active':
if (res.success) {
dt += res.total;
count.value = dt;
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg;
id.value = res.source.id;
isSource.value = true;
} else {
errMessage.value = res.message;
}
}
break;
case 'sync':
dt += res;
count.value = dt;
break;
case 'import':
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message;
}
break;
default:
break;
}
};
_source.onerror = () => {
flag.value = false
_source.close();
flag.value = false;
_source.close();
};
_source.onopen = () => {};
}
};
watch(() => props.api,
watch(
() => props.api,
(newValue) => {
if(newValue) {
getData(newValue)
if (newValue) {
getData(newValue);
}
},
{deep: true, immediate: true}
)
},
{ deep: true, immediate: true },
);
</script>

View File

@ -9,12 +9,7 @@
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<j-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="modelRef"
>
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-row type="flex">
<j-col flex="180px">
<j-form-item name="photoUrl">
@ -22,14 +17,33 @@
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item name="id">
<j-form-item
name="id"
:rules="[
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
max: 64,
message: '最多输入64个字符',
},
{
validator: vailId,
trigger: 'blur',
},
]"
>
<template #label>
<span>
ID
<j-tooltip title="若不填写系统将自动生成唯一ID">
<j-tooltip
title="若不填写系统将自动生成唯一ID"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
@ -39,7 +53,20 @@
:disabled="!!data?.id"
/>
</j-form-item>
<j-form-item label="名称" name="name">
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]"
>
<j-input
v-model:value="modelRef.name"
placeholder="请输入名称"
@ -47,13 +74,23 @@
</j-form-item>
</j-col>
</j-row>
<j-form-item name="productId">
<j-form-item
name="productId"
:rules="[
{
required: true,
message: '请选择所属产品',
},
]"
>
<template #label>
<span>所属产品
<span
>所属产品
<j-tooltip title="只能选择“正常”状态的产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px" />
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
@ -68,10 +105,20 @@
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{item.name}}</j-select-option>
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
<j-form-item label="说明" name="describe">
<j-form-item
label="说明"
name="describe"
:rules="[
{
max: 200,
message: '最多输入200个字符'
},
]"
>
<j-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
@ -123,45 +170,6 @@ const vailId = async (_: Record<string, any>, value: string) => {
}
};
const rules = {
name: [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
],
photoUrl: [
{
required: true,
message: '请上传图标',
},
],
productId: [
{
required: true,
message: '请选择所属产品',
},
],
id: [
{
max: 64,
message: '最多输入64个字符',
},
{
pattern: /^[j-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
validator: vailId,
trigger: 'blur',
},
],
};
watch(
() => props.data,
(newValue) => {
@ -199,13 +207,13 @@ const handleSave = () => {
.validate()
.then(async (_data: any) => {
loading.value = true;
const obj = {...toRaw(modelRef), ..._data}
if(!obj.id){
delete obj.id
const obj = { ..._data };
if (!obj.id) {
delete obj.id;
}
const resp = await update(obj).finally(() => {
loading.value = false;
})
});
if (resp.status === 200) {
message.success('操作成功!');
emit('save');

View File

@ -14,7 +14,6 @@
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
>
<template #headerTitle>
@ -253,6 +252,7 @@
@close="operationVisible = false"
:api="api"
:type="type"
@save="onRefresh"
/>
<Save
v-if="visible"
@ -315,6 +315,7 @@ const columns = [
key: 'id',
search: {
type: 'string',
defaultTermType: 'eq'
},
},
{
@ -323,6 +324,7 @@ const columns = [
key: 'name',
search: {
type: 'string',
first: true
},
},
{
@ -390,6 +392,7 @@ const columns = [
hideInTable: true,
search: {
type: 'select',
rename: 'productId$product-info',
options: () =>
new Promise((resolve) => {
getProviders().then((resp: any) => {
@ -643,10 +646,6 @@ const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);

View File

@ -306,23 +306,6 @@ const columns = [
const _selectedRowKeys = ref<string[]>([]);
const currentForm = ref({});
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
// const handleClick = (dt: any) => {
// if (_selectedRowKeys.value.includes(dt.id)) {
// const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
// _selectedRowKeys.value.splice(_index, 1);
// } else {
// _selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
// }
// };
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',

View File

@ -1,5 +1,5 @@
<template>
<a-form-item label="标识" name="id" :rules="[
<j-form-item label="标识" name="id" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -7,14 +7,14 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'"></a-input>
</a-form-item>
<a-form-item label="名称" name="name" :rules="[
<j-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'"></j-input>
</j-form-item>
<j-form-item label="名称" name="name" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="value.name" size="small"></a-input>
</a-form-item>
<j-input v-model:value="value.name" size="small"></j-input>
</j-form-item>
<template v-if="modelType === 'properties'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"
@change-type="changeValueType"></value-type-form>
@ -22,42 +22,42 @@
:valueType="value.valueType"></expands-form>
</template>
<template v-if="modelType === 'functions'">
<a-form-item label="是否异步" name="async" :rules="[
<j-form-item label="是否异步" name="async" :rules="[
{ required: true, message: '请选择是否异步' },
]">
<a-radio-group v-model:value="value.async">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="输入参数" name="inputs" :rules="[
{ required: true, message: '请输入输入参数' },
<j-radio-group v-model:value="value.async">
<j-radio :value="true"></j-radio>
<j-radio :value="false"></j-radio>
</j-radio-group>
</j-form-item>
<j-form-item label="输入参数" name="inputs" :rules="[
{ required: true, validator: (_rule: Rule, val: Record<any, any>[]) => validateJson(_rule, val, '输入参数') },
]">
<JsonParam v-model:value="value.inputs" :name="['inputs']"></JsonParam>
</a-form-item>
<value-type-form :name="['output']" v-model:value="value.output" key="function" title="输出参数"></value-type-form>
</j-form-item>
<value-type-form :name="['output']" v-model:value="value.output" key="function" title="输出参数" :required="false"></value-type-form>
</template>
<template v-if="modelType === 'events'">
<a-form-item label="级别" :name="['expands', 'level']" :rules="[
<j-form-item label="级别" :name="['expands', 'level']" :rules="[
{ required: true, message: '请选择级别' },
]">
<a-select v-model:value="value.expands.level" :options="EventLevel" size="small"></a-select>
</a-form-item>
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="function" title="输出参数"></value-type-form>
<j-select v-model:value="value.expands.level" :options="EventLevel" size="small"></j-select>
</j-form-item>
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="function" title="输出参数" only-object></value-type-form>
</template>
<template v-if="modelType === 'tags'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"></value-type-form>
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
{ required: true, message: '请选择读写类型' },
<j-form-item label="标签类型" :name="['expands', 'type']" :rules="[
{ required: true, message: '请选择标签类型' },
]">
<a-select v-model:value="value.expands.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
<j-select v-model:value="value.expands.type" :options="ExpandsTypeList" mode="multiple" size="small"></j-select>
</j-form-item>
</template>
<a-form-item label="说明" name="description" :rules="[
<j-form-item label="说明" name="description" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="value.description" size="small"></a-textarea>
</a-form-item>
<j-textarea v-model:value="value.description" size="small"></j-textarea>
</j-form-item>
</template>
<script setup lang="ts" name="BaseForm">
import { PropType } from 'vue';
@ -68,6 +68,8 @@ import { getMetadataConfig } from '@/api/device/product'
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
import { EventLevel, ExpandsTypeList } from '@/views/device/data';
import { useMetadataStore } from '@/store/metadata';
import { validateJson } from './validator';
import { Rule } from 'ant-design-vue/es/form';
const props = defineProps({
type: {

View File

@ -1,26 +1,30 @@
<template>
<a-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
<j-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
{ required: true, message: '请选择来源' },
]">
<a-select v-model:value="_value.source" :options="PropertySource" size="small"
:disabled="metadataStore.model.action === 'edit'"></a-select>
</a-form-item>
<j-select v-model:value="_value.source" :options="PropertySource" size="small"
:disabled="metadataStore.model.action === 'edit'" @change="changeSource"></j-select>
</j-form-item>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule"
:name="name.concat(['virtualRule'])" :id="id" :showWindow="_value.source === 'rule'"></virtual-rule-param>
<a-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
<j-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="_value.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
<a-form-item label="其他配置" v-if="config.length > 0">
<a-form-item v-for="(item, index) in config" :key="index">
<j-select v-model:value="_value.type" :options="ExpandsTypeList" mode="multiple" size="small" :disabled="['manual', 'rule'].includes(_value.source)"></j-select>
</j-form-item>
<j-form-item label="其他配置" v-if="config.length > 0">
<j-form-item v-for="(item, index) in config" :key="index">
<config-param v-model:value="_value" :config="item" :name="name"></config-param>
</a-form-item>
</a-form-item>
<a-form-item v-if="type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(valueType.type)"
label="指标配置" :name="name.concat(['metrics'])">
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType" :name="name.concat(['metrics'])"></metrics-param>
</a-form-item>
</j-form-item>
</j-form-item>
<j-form-item
v-if="type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(valueType.type)"
label="指标配置" :name="name.concat(['metrics'])" :rules="[
{ validator: () => validateMetrics(_value.metrics), message: '请输入指标配置' }
]">
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType"
:name="name.concat(['metrics'])"></metrics-param>
</j-form-item>
</template>
<script setup lang="ts" name="ExpandsForm">
import { useMetadataStore } from '@/store/metadata';
@ -82,9 +86,29 @@ const metadataStore = useMetadataStore()
onMounted(() => {
if (props.type === 'product' || !props.value?.source) {
emit('update:value', { ...props.value, source: 'device' })
emit('update:value', { source: 'device', ...props.value })
}
})
const validateMetrics = (value: Record<any, any>[]) => {
const flag = value.every((item) => {
return item.id && item.name && item.value;
});
if (!flag) {
return Promise.reject(new Error('请输入指标配置'));
}
return Promise.resolve();
}
const changeSource = (val: string) => {
if (val === 'manual') {
_value.value.type = ['write']
} else if (val === 'rule') {
_value.value.type = ['report']
} else {
_value.value.type = []
}
}
</script>
<style lang="less" scoped></style>

View File

@ -1,48 +1,54 @@
<template>
<a-form-item :label="title" :name="name.concat(['type'])" :rules="[
metadataStore.model.type !== 'functions' ? { required: true, message: `请选择${title}` } : {},
<j-form-item :label="title" :name="name.concat(['type'])" :rules="[
required ? { required: true, message: `请选择${title}` } : {},
]">
<a-select v-model:value="_value.type" :options="metadataStore.model.type === 'events' ? eventDataTypeList : _dataTypeList" size="small" @change="changeType"></a-select>
</a-form-item>
<a-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(_value.type)">
<j-select v-model:value="_value.type"
:options="onlyObject ? eventDataTypeList : _dataTypeList" size="small"
@change="changeType"></j-select>
</j-form-item>
<j-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(_value.type)">
<InputSelect v-model:value="_value.unit" :options="unit.unitOptions" size="small"></InputSelect>
</a-form-item>
<a-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(_value.type)">
<a-input-number v-model:value="_value.scale" size="small" :min="0" :max="2147483647" :precision="0" :default-value="2"
style="width: 100%"></a-input-number>
</a-form-item>
<a-form-item label="布尔值" name="booleanConfig" v-if="['boolean'].includes(_value.type)">
</j-form-item>
<j-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(_value.type)">
<j-input-number v-model:value="_value.scale" size="small" :min="0" :max="2147483647" :precision="0"
style="width: 100%"></j-input-number>
</j-form-item>
<j-form-item label="布尔值" name="booleanConfig" v-if="['boolean'].includes(_value.type)">
<BooleanParam :name="name" v-model:value="_value"></BooleanParam>
</a-form-item>
<a-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(_value.type)" :rules="[
{ required: true, message: '请配置枚举项' }
</j-form-item>
<j-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(_value.type)" :rules="[
{ required: true, validator: validateEnum }
]">
<EnumParam v-model:value="_value.elements" :name="name.concat(['elements'])"></EnumParam>
</a-form-item>
<a-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(_value.type)">
</j-form-item>
<j-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(_value.type)">
<template #label>
<a-space>
<j-space>
最大长度
<a-tooltip title="字节">
<j-tooltip title="字节">
<question-circle-outlined style="color: rgb(136, 136, 136); font-size: 12px;" />
</a-tooltip>
</a-space>
</j-tooltip>
</j-space>
</template>
<a-input-number v-model:value="_value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
style="width: 100%;"></a-input-number>
</a-form-item>
<a-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(_value.type)">
<j-input-number v-model:value="_value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
style="width: 100%;"></j-input-number>
</j-form-item>
<j-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(_value.type)" :rules="[
{ validator: validateArray }
]">
<ArrayParam v-model:value="_value.elementType" :name="name.concat(['elementType'])"></ArrayParam>
</a-form-item>
<a-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(_value.type)" :rules="[]">
</j-form-item>
<j-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(_value.type)" :rules="[
{ validator: (_rule: Rule, val: Record<any, any>[]) => validateJson(_rule, val, 'JSON对象') }
]">
<JsonParam v-model:value="_value.properties" :name="name.concat(['properties'])"></JsonParam>
</a-form-item>
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(_value.type)" initialValue="url"
</j-form-item>
<j-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(_value.type)" initialValue="url"
:rules="[
{ required: true, message: '请选择文件类型' },
]">
<a-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></a-select>
</a-form-item>
<j-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></j-select>
</j-form-item>
</template>
<script lang="ts" setup mame="BaseForm">
import { DataTypeList, FileTypeList } from '@/views/device/data';
@ -56,6 +62,8 @@ import EnumParam from '@/components/Metadata/EnumParam/index.vue'
import ArrayParam from '@/components/Metadata/ArrayParam/index.vue'
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
import { useMetadataStore } from '@/store/metadata';
import { validateEnum, validateArray, validateJson } from './validator'
import { Rule } from 'ant-design-vue/es/form';
type ValueType = Record<any, any>;
const props = defineProps({
@ -73,8 +81,16 @@ const props = defineProps({
required: true
},
title: {
String,
type: String,
default: '数据类型'
},
required: {
type: Boolean,
default: true
},
onlyObject: {
type: Boolean,
default: false
}
})
@ -84,15 +100,8 @@ interface Emits {
}
const emit = defineEmits<Emits>()
// emit('update:value', { extends: {}, ...props.value })
const metadataStore = useMetadataStore()
// const _value = computed({
// get: () => props.value,
// set: val => {
// emit('update:value', val)
// }
// })
const _value = ref<ValueType>({})
watchEffect(() => {
_value.value = props.value || {
@ -107,7 +116,7 @@ watch(_value,
{ deep: true, immediate: true })
onMounted(() => {
if (metadataStore.model.type === 'events') {
if (props.onlyObject) {
_value.value = {
type: 'object',
expands: {}
@ -140,8 +149,30 @@ const eventDataTypeList = [
]
const changeType = (val: SelectValue) => {
if (['float', 'double'].includes(_value.value.type) && _value.value.scale === undefined) {
_value.value.scale = 2
}
emit('changeType', val as string)
}
// const rules = ref({
// type: [
// metadataStore.model.type !== 'functions' ? { required: true, message: `${props.title}` } : {},
// ],
// elements: [
// { required: true, validator: validateEnum, message: '' }
// ],
// elementType: [
// { validator: validateArray, message: '' }
// ],
// properties: [
// { validator: validateJson, message: '' }
// ],
// fileType: [
// { required: true, message: '' },
// ]
// })
</script>
<style lang="less" scoped>
:deep(.ant-form-item-label) {

View File

@ -1,13 +1,13 @@
<template>
<a-drawer :mask-closable="false" width="25vw" visible :title="`${title}-${typeMapping[metadataStore.model.type]}`"
<j-drawer :mask-closable="false" width="25vw" visible :title="`${title}-${typeMapping[metadataStore.model.type]}`"
@close="close" destroy-on-close :z-index="1000" placement="right">
<template #extra>
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
<j-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</j-button>
</template>
<a-form ref="formRef" :model="form.model" layout="vertical">
<j-form ref="formRef" :model="form.model" layout="vertical">
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form>
</a-drawer>
</j-form>
</j-drawer>
</template>
<script lang="ts" setup name="Edit">
import { useInstanceStore } from '@/store/instance';
@ -22,6 +22,7 @@ import { DeviceInstance } from '@/views/device/Instance/typings';
import BaseForm from './BaseForm.vue';
import { PropType } from 'vue';
import { _deploy } from '@/api/device/product';
import { cloneDeep } from 'lodash';
const props = defineProps({
type: {
@ -60,7 +61,7 @@ const form = reactive({
model: {} as any,
})
if (metadataStore.model.action === 'edit') {
form.model = metadataStore.model.item
form.model = cloneDeep(metadataStore.model.item)
}
const formRef = ref<FormInstance>()
@ -75,7 +76,9 @@ const save = reactive({
const type = metadataStore.model.type
const _detail: ProductItem | DeviceInstance = props.type === 'device' ? instanceStore.detail : productStore.current
const _metadata = JSON.parse(_detail?.metadata || '{}')
const list = _metadata[type] as any[]
console.log(_metadata)
console.log(type)
const list = (_metadata[type] as any[]) || []
if (formValue.id) {
if (metadataStore.model.action === 'add' && list.some(item => item.id === formValue.id)) {
message.error('标识已存在')

View File

@ -0,0 +1,60 @@
import { Rule } from "ant-design-vue/es/form";
export const validateEnum = async (_rule: Rule, val: Record<any, any>[]) => {
if (val.length === 0) return Promise.reject(new Error('请配置枚举项'));
const flag = val.every((item) => {
return item.value && item.text;
});
if (!flag) {
return Promise.reject(new Error('请配置枚举项'));
}
return Promise.resolve();
}
export const validateArray = async (_rule: Rule, val: Record<any, any>) => {
if (!val) return Promise.reject(new Error(`请输入元素配置`));
await validateValueType(_rule, val)
return Promise.resolve();
}
export const validateJson = async (_rule: Rule, val: Record<any, any>[], title = '配置参数') => {
if (!val || val.length === 0) {
return Promise.reject(new Error(`请输入${title}`));
}
for (let item of val) {
if (!item) return Promise.reject(new Error(`请输入${title}`));
await validateIdName(_rule, item)
await validateValueType(_rule, item.valueType)
}
return Promise.resolve();
}
export const validateIdName = async (_rule: Rule, val: Record<any, any>) => {
if (!val.id) {
return Promise.reject(new Error('请输入标识'))
}
if (!val.name) {
return Promise.reject(new Error('请输入名称'))
}
}
export const validateValueType = async (_rule: Rule, val: Record<any, any>, title = '数据类型') => {
console.log(val)
if (!val) return Promise.reject(new Error('请输入元素配置'));
if (!val?.type) {
return Promise.reject(new Error(`请选择${title}`))
}
if (['enum'].includes(val.type)) {
await validateEnum(_rule, val.elements)
}
if (['array'].includes(val.type)) {
await validateArray(_rule, val.elementType)
}
if (['object'].includes(val.type)) {
await validateJson(_rule, val.properties)
}
if (['file'].includes(val.type) && !val.fileType) {
return Promise.reject(new Error('请选择文件类型'))
}
return Promise.resolve();
}

View File

@ -1,9 +1,9 @@
<template>
<j-pro-table :loading="loading" :data-source="data" size="small" :columns="columns" row-key="id" model="TABLE">
<template #headerTitle>
<a-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></a-input-search>
</template>
<template #rightExtraRender>
<div class="table-header">
<div>
<j-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></j-input-search>
</div>
<div>
<PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick"
:disabled="operateLimits('add', type)" :tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
@ -14,45 +14,50 @@
新增
</PermissionButton>
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type" @refresh="refreshMetadata"></Edit>
</div>
</div>
<a-table :loading="loading" :data-source="data" :columns="columns" row-key="id" model="TABLE" size="small"
:pagination="pagination">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'level'">
{{ levelMap[record.expands?.level] || '-' }}
</template>
<template v-if="column.dataIndex === 'async'">
{{ record.async ? '是' : '否' }}
</template>
<template v-if="column.dataIndex === 'valueType'">
{{ record.valueType?.type }}
</template>
<template v-if="column.dataIndex === 'source'">
{{ sourceMap[record.expands?.source] }}
</template>
<template v-if="column.dataIndex === 'type'">
<j-tag v-for="item in (record.expands?.type || [])" :key="item">
{{ expandsType[item] }}
</j-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<j-space>
<PermissionButton :has-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
:udisabled="operateLimits('updata', type)" @click="handleEditClick(record)" :tooltip="{
title: operateLimits('updata', type) ? '当前的存储方式不支持编辑' : '编辑',
}">
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton :has-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
:pop-confirm="{
title: '确认删除?', onConfirm: async () => {
await removeItem(record);
},
}" :tooltip="{
title: '删除',
}">
<AIcon type="DeleteOutlined" />
</PermissionButton>
</j-space>
</template>
</template>
<template #level="slotProps">
{{ levelMap[slotProps.expands?.level] || '-' }}
</template>
<template #async="slotProps">
{{ slotProps.async ? '是' : '否' }}
</template>
<template #valueType="slotProps">
{{ slotProps.valueType?.type }}
</template>
<template #source="slotProps">
{{ sourceMap[slotProps.expands?.source] }}
</template>
<template #type="slotProps">
<j-tag v-for="item in (slotProps.expands?.type || [])" :key="item">
{{ expandsType[item] }}
</j-tag>
</template>
<template #action="slotProps">
<j-space>
<PermissionButton :uhas-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
:udisabled="operateLimits('updata', type)" @click="handleEditClick(slotProps)" :tooltip="{
title: operateLimits('updata', type) ? '当前的存储方式不支持编辑' : '编辑',
}">
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton :uhas-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
:pop-confirm="{
title: '确认删除?', onConfirm: async () => {
await removeItem(slotProps);
},
}" :tooltip="{
title: '删除',
}">
<Aicon type="DeleteOutlined" />
</PermissionButton>
</j-space>
</template>
</j-pro-table>
</a-table>
</template>
<script setup lang="ts" name="BaseMetadata">
import type { MetadataItem, MetadataType } from '@/views/device/Product/typings'
@ -61,14 +66,10 @@ import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product'
import { useMetadataStore } from '@/store/metadata'
import PermissionButton from '@/components/PermissionButton/index.vue'
import { message } from 'ant-design-vue/es'
import { SystemConst } from '@/utils/consts'
import { Store } from 'jetlinks-store'
import { TablePaginationConfig, message } from 'ant-design-vue/es'
import { asyncUpdateMetadata, removeMetadata } from '../metadata'
import { detail } from '@/api/device/instance'
import Edit from './Edit/index.vue'
// import { detail } from '@/api/device/instance'
// import { detail as productDetail } from '@/api/device/product'
interface Props {
type: MetadataType;
target: 'product' | 'device';
@ -106,6 +107,15 @@ const actions = [
scopedSlots: true,
},
];
const pagination = {
showTotal: (num: number, range: number[]) => {
return `${range[0]} - ${range[1]} 条/总共 ${num}`;
},
showSizeChanger: true,
showQuickJumper: false,
defaultPageSize: 10,
size: 'small',
} as TablePaginationConfig
const columns = computed(() => MetadataMapping.get(type)!.concat(actions))
const items = computed(() => JSON.parse((target === 'product' ? productStore.current?.metadata : instanceStore.current.metadata) || '{}') as MetadataItem[])
const searchValue = ref<string>()
@ -118,10 +128,6 @@ const handleSearch = (searchValue: string) => {
}
}
onMounted(() => {
})
const refreshMetadata = () => {
loading.value = true
// const res = target === 'product'
@ -135,6 +141,13 @@ const refreshMetadata = () => {
watch([route.params.id, type], refreshMetadata, { immediate: true })
const metadataStore = useMetadataStore()
watch(() => metadataStore.model.importMetadata,
(val: boolean) => {
if (!!val) {
refreshMetadata()
metadataStore.set('importMetadata', false)
}
})
const handleAddClick = () => {
metadataStore.set('edit', true)
metadataStore.set('item', undefined)
@ -165,11 +178,18 @@ const handleEditClick = (record: MetadataItem) => {
}
const resetMetadata = async () => {
// const { id } = route.params
// const resp = await detail(id as string);
// if (resp.status === 200) {
// instanceStore.setCurrent(resp?.result || []);
// }
const { id } = route.params
const resp = await detail(id as string);
if (resp.status === 200) {
instanceStore.setCurrent(resp?.result || []);
if (target === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
metadataStore.set('importMetadata', true)
};
const removeItem = async (record: MetadataItem) => {
@ -180,7 +200,7 @@ const removeItem = async (record: MetadataItem) => {
const result = await asyncUpdateMetadata(target, _currentData);
if (result.status === 200) {
message.success('操作成功!');
Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
metadataStore.model.edit = false;
metadataStore.model.item = {};
resetMetadata();
@ -190,5 +210,9 @@ const removeItem = async (record: MetadataItem) => {
};
</script>
<style scoped lang="less">
.table-header {
display: flex;
justify-content: space-between;
padding: 16px 0;
}
</style>

View File

@ -1,13 +1,13 @@
<template>
<a-drawer :mask-closable="false" title="查看物模型" width="700" v-model:visible="_visible" destroy-on-close @close="close">
<j-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">
<j-space>
<j-button type="primary" @click="handleExport">
导出
</a-button>
</a-space>
</j-button>
</j-space>
</template>
<a-spin :spinning="loading">
<j-spin :spinning="loading">
<div class="cat-content">
<p class="cat-tip">
物模型是对设备在云端的功能描述包括设备的属性服务和事件物联网平台通过定义一种物的描述语言来描述物模型称之为
@ -15,15 +15,15 @@
组装上报设备的数据您可以导出完整物模型用于云端应用开发
</p>
</div>
<a-tabs @change="handleConvertMetadata" destroy-inactive-tab-pane>
<a-tab-pane v-for="item in codecs" :key="item.id" :tab="item.name">
<j-tabs @change="handleConvertMetadata" destroy-inactive-tab-pane>
<j-tab-pane v-for="item in codecs" :key="item.id" :tab="item.name">
<div class="cat-panel">
<MonacoEditor v-model="value" theme="vs" style="height: 100%"></MonacoEditor>
</div>
</a-tab-pane>
</a-tabs>
</a-spin>
</a-drawer>
</j-tab-pane>
</j-tabs>
</j-spin>
</j-drawer>
</template>
<script setup lang="ts" name="Cat">
import { message } from 'ant-design-vue/es';

View File

@ -1,5 +1,5 @@
<template>
<a-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
<j-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">
@ -7,46 +7,46 @@
导入的物模型会覆盖原来的属性功能事件标签请谨慎操作
</p>
</div>
<a-form layout="vertical" v-model="formModel">
<a-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<a-select 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"
<j-form layout="vertical" v-model="formModel">
<j-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<j-select v-model:value="formModel.type">
<j-select-option value="copy">拷贝产品</j-select-option>
<j-select-option value="import">导入物模型</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
<j-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></j-select>
</j-form-item>
<j-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"
<j-select v-model:value="formModel.metadata">
<j-select-option value="jetlinks">Jetlinks物模型</j-select-option>
<j-select-option value="alink">阿里云物模型TSL</j-select-option>
</j-select>
</j-form-item>
<j-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-input v-model:value="formModel.upload">
<j-select v-model:value="formModel.metadataType">
<j-select-option value="file">文件上传</j-select-option>
<j-select-option value="script">脚本</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
<j-input v-model:value="formModel.upload">
<template #addonAfter>
<a-upload v-model:file-list="fileList" :before-upload="beforeUpload" accept=".json"
<j-upload v-model:file-list="fileList" :before-upload="beforeUpload" accept=".json"
:show-upload-list="false" :action="FILE_UPLOAD" @change="fileChange"
:headers="{ 'X-Access-Token': getToken()}">
<AIcon type="UploadOutlined" class="upload-button" />
</a-upload>
</j-upload>
</template>
</a-input>
</a-form-item>
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
</j-input>
</j-form-item>
<j-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
<MonacoEditor v-model="formModel.import" theme="vs" style="height: 300px"></MonacoEditor>
</a-form-item>
</a-form>
</a-modal>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts" name="Import">
import { useForm } from 'ant-design-vue/es/form';
@ -54,13 +54,14 @@ import { saveMetadata } from '@/api/device/instance'
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
import type { DefaultOptionType } from 'ant-design-vue/es/select';
import type { UploadProps, UploadFile, UploadChangeParam } from 'ant-design-vue/es';
import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings'
import { message } from 'ant-design-vue/es';
import type { DeviceMetadata } from '@/views/device/Product/typings'
import { message } from 'jetlinks-ui-components';
import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product';
import { FILE_UPLOAD } from '@/api/comm';
import { getToken } from '@/utils/comm';
import MonacoEditor from '@/components/MonacoEditor/index.vue'
import { useMetadataStore } from '@/store/metadata';
const route = useRoute()
const instanceStore = useInstanceStore()
@ -87,7 +88,6 @@ const _visible = computed({
})
const close = () => {
console.log(1)
emits('update:visible', false);
}
@ -196,7 +196,7 @@ const operateLimits = (mdata: DeviceMetadata) => {
});
return obj;
};
const metadataStore = useMetadataStore()
const handleImport = async () => {
validate().then(async (data) => {
loading.value = true
@ -224,6 +224,7 @@ const handleImport = async () => {
} else {
productStore.refresh(id as string)
}
metadataStore.set('importMetadata', true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close()
@ -263,13 +264,14 @@ const handleImport = async () => {
message.success('导入成功')
}
}
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
if (props?.type === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
metadataStore.set('importMetadata', true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close();
} catch (e) {
loading.value = false

View File

@ -1,58 +1,91 @@
<template>
<a-card class="device-count-container">
<template #title>
<h5 class="title">基础统计</h5>
</template>
<template #extra>
<span style="color: #1d39c4; cursor: pointer" @click="jumpPage"
>详情</span
>
</template>
<div class="device-count-container">
<h5 class="title">基础统计</h5>
<span class="detail" @click="jumpPage('link/DashBoard')"> 详情 </span>
<div class="box-list">
<div class="box-item">
<div class="label">CPU使用率</div>
<div class="value">{{ cpu }}</div>
<div class="value">{{ cpu + '%' }}</div>
<Pie
class="chart"
:value="cpu"
chart-ref="cpuChart"
:value="20"
:color-arr="['#ebebeb', '#d3adf7']"
image="/images/home/top-3.svg"
/>
</div>
<div class="box-item">
<div class="label">JVM内存</div>
<div class="value">{{ jvm }}</div>
<div class="value">{{ jvm + '%' }}</div>
<Pie
class="chart"
chart-ref="jvmChart"
:value="31"
:value="jvm"
:color-arr="['#d6e4ff', '#85a5ff']"
image="/images/home/top-4.svg"
/>
</div>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { getWebSocket } from '@/utils/websocket';
import Pie from './Pie.vue';
import { map } from 'rxjs/operators';
import { useMenuStore } from '@/store/menu';
const cpu = ref('20%');
const jvm = ref('31%');
const cpu = ref(0);
const jvm = ref(0);
const getData = ()=>{
}
const { jumpPage } = useMenuStore();
const jumpPage = () => {};
const cpuSocket = getWebSocket(
'operations-statistics-system-info-cpu-realTime',
'/dashboard/systemMonitor/stats/info/realTime',
{
type: 'cpu',
interval: '2s',
agg: 'avg',
},
)
?.pipe(map((res: any) => res.payload))
.subscribe((resp: any) => {
cpu.value = resp.value?.systemUsage || 0;
});
const jvmSocket = getWebSocket(
`operations-statistics-system-info-memory-realTime`,
`/dashboard/systemMonitor/stats/info/realTime`,
{
type: 'memory',
interval: '2s',
agg: 'avg',
},
)
?.pipe(map((res: any) => res.payload))
.subscribe((payload: any) => {
jvm.value = payload.value?.jvmHeapUsage || 0;
});
onUnmounted(() => {
cpuSocket && cpuSocket.unsubscribe();
jvmSocket && jvmSocket.unsubscribe();
});
</script>
<style lang="less" scoped>
.device-count-container {
:deep(.ant-card-body) {
padding-top: 0;
background-color: #fff;
padding: 24px 14px;
position: relative;
.detail {
color: #1d39c4;
cursor: pointer;
position: absolute;
right: 12px;
top: 24px;
z-index: 3;
}
.title {
position: relative;

View File

@ -6,7 +6,7 @@
<div
class="box-item"
v-for="(item, index) in cardData"
@click="jumpPage(item.link,item.params)"
@click="jumpPage(item)"
>
<div class="item-english">{{ item.english }}</div>
<div class="item-title">{{ item.label }}</div>
@ -21,10 +21,11 @@
</template>
<script setup lang="ts">
import { bootConfig } from "../typing";
import { bootConfig } from '../typing';
import { useMenuStore } from '@/store/menu';
import { message } from 'ant-design-vue';
const { jumpPage } = useMenuStore();
const { jumpPage: _jumpPage } = useMenuStore();
const props = defineProps({
cardData: Array<bootConfig>,
@ -32,6 +33,10 @@ const props = defineProps({
});
const { cardData, cardTitle } = toRefs(props);
const jumpPage = (item: bootConfig) => {
if (item.auth === undefined || item.auth) _jumpPage(item.link, item.params);
else message.warning('暂无权限,请联系管理员');
};
</script>
<style lang="less" scoped>

View File

@ -28,34 +28,19 @@
<script setup lang="ts">
import { message } from 'ant-design-vue';
import { bootConfig } from "../typing";
import { bootConfig } from '../typing';
import { useMenuStore } from '@/store/menu';
const router = useRouter();
const props = defineProps({
cardData: Array<bootConfig>,
cardTitle: String,
});
const { cardData, cardTitle } = toRefs(props);
const { jumpPage: _jumpPage } = useMenuStore();
const jumpPage = (row: bootConfig): void => {
if (row.auth && row.link) {
router.push(`${row.link}${objToParams(row.params || {})}`);
} else {
message.warning('暂无权限,请联系管理员');
}
};
const objToParams = (source: object): string => {
if (Object.prototype.toString.call(source) === '[object Object]') {
const paramsArr = <any>[];
// 使for ints
Object.entries(source).forEach(([prop, value]) => {
if (typeof value === 'object') value = JSON.stringify(value);
paramsArr.push(`${prop}=${value}`);
});
if (paramsArr.length > 0) return '?' + paramsArr.join('&');
}
return '';
const jumpPage = (item: bootConfig) => {
if (item.auth === undefined || item.auth) _jumpPage(item.link, item.params);
else message.warning('暂无权限,请联系管理员');
};
</script>

View File

@ -1,24 +1,24 @@
<template>
<div class="comprehensive-home-conatiner">
<a-row :gutter="24" class="top" style="margin-bottom: 24px">
<a-col :span="6" class="left">
<j-row :gutter="24" class="top" style="margin-bottom: 24px">
<j-col :span="6" class="left">
<BootCardSmall
:cardData="deviceBootConfig"
cardTitle="物联网引导"
/>
<div style="width: 100%; height: 24px"></div>
<BootCardSmall :cardData="opsBootConfig" cardTitle="运维引导" />
</a-col>
<a-col :span="18" class="right">
<a-row :gutter="24">
<a-col :span="12"><DeviceCountCard /></a-col>
<a-col :span="12"><BasicCountCard /></a-col>
<a-col :span="24" style="margin-top: 24px">
</j-col>
<j-col :span="18" class="right">
<j-row :gutter="24">
<j-col :span="12"><DeviceCountCard /></j-col>
<j-col :span="12"><BasicCountCard /></j-col>
<j-col :span="24" style="margin-top: 24px">
<PlatformPicCard image="/images/home/content1.png" />
</a-col>
</a-row>
</a-col>
</a-row>
</j-col>
</j-row>
</j-col>
</j-row>
<StepCard
cardTitle="设备接入推荐步骤"
@ -31,19 +31,36 @@
tooltip="请根据业务需要对下述步骤进行选择性操作。"
:dataList="opsStepDetails"
/>
<div class="dialog">
<ProductChooseDialog
v-if="productDialogVisible"
v-model:visible="productDialogVisible"
@confirm="(id:string)=>jumpPage('device/Product/Detail', { id, tab: 'Device'})"
/>
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id })"
/>
</div>
</div>
</template>
<script setup lang="ts" name="ComprehensiveHome">
import ProductChooseDialog from '../dialogs/ProductChooseDialog.vue';
import DeviceChooseDialog from '../dialogs/DeviceChooseDialog.vue';
import BootCardSmall from '../BootCardSmall.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue';
import { useMenuStore } from '@/store/menu';
import { usePermissionStore } from '@/store/permission';
import { recommendList, bootConfig } from '../../typing';
import { useMenuStore } from '@/store/menu';
const { jumpPage } = useMenuStore();
//
const hasPermission = usePermissionStore().hasPermission;
@ -53,16 +70,15 @@ const devicePermission = (action: string) =>
hasPermission(`device/Instance:${action}`);
const rulePermission = (action: string) =>
hasPermission(`rule-engine/Instance:${action}`);
//
const menuPermission = useMenuStore().hasPermission;
// -
const _deviceBootConfig: bootConfig[] = [
const deviceBootConfig: bootConfig[] = [
{
english: 'STEP1',
label: '创建产品',
link: '/iot/device/Product',
auth: productPermission('add'),
image: '/images/home/guide-home1.png',
params: {
save: true,
},
@ -72,8 +88,9 @@ const _deviceBootConfig: bootConfig[] = [
label: '创建设备',
link: '/iot/device/Instance',
auth: devicePermission('add'),
image: '/images/home/guide-home1.png',
params: {
save: true,
type: 'add',
},
},
{
@ -81,20 +98,12 @@ const _deviceBootConfig: bootConfig[] = [
label: '规则引擎',
link: '/iot/rule-engine/Instance',
auth: rulePermission('add'),
image: '/images/home/guide-home3.png',
params: {
save: true,
},
},
];
const deviceImages = [
'/images/home/guide-home1.png',
'/images/home/guide-home2.png',
'/images/home/guide-home3.png',
];
const deviceBootConfig = _deviceBootConfig.map((item, i) => ({
...item,
image: deviceImages[i],
}));
// -
const deviceStepDetails: recommendList[] = [
@ -103,7 +112,7 @@ const deviceStepDetails: recommendList[] = [
details:
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
iconUrl: '/images/home/bottom-4.png',
linkUrl: '/iot/device/Product',
linkUrl: 'device/Product',
auth: productPermission('add'),
params: {
save: true,
@ -114,18 +123,20 @@ const deviceStepDetails: recommendList[] = [
details:
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
iconUrl: '/images/home/bottom-1.png',
linkUrl: '/iot/device/Product/detail',
linkUrl: 'device/Product/Detail',
auth: productPermission('update'),
dialogTag: 'accessMethod',
onClick: () => {
productDialogVisible.value = true;
},
},
{
title: '添加测试设备',
details: '添加单个设备,用于验证产品模型是否配置正确。',
iconUrl: '/images/home/bottom-5.png',
linkUrl: '/iot/device/Instance',
linkUrl: 'device/Instance',
auth: devicePermission('add'),
params: {
save: true,
type: 'add',
},
},
{
@ -133,16 +144,17 @@ const deviceStepDetails: recommendList[] = [
details:
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
iconUrl: '/images/home/bottom-2.png',
linkUrl: '/iot/device/Instance/detail',
// auth: devicePermission('update'),
auth: true,
dialogTag: 'funcTest',
linkUrl: 'device/Instance/Detail',
auth: devicePermission('update'),
onClick: () => {
deviceDialogVisible.value = true;
},
},
{
title: '批量添加设备',
details: '批量添加同一产品下的设备',
iconUrl: '/images/home/bottom-3.png',
linkUrl: '/iot/device/Instance',
linkUrl: 'device/Instance',
auth: devicePermission('import'),
params: {
import: true,
@ -151,41 +163,29 @@ const deviceStepDetails: recommendList[] = [
];
// -
const _opsBootConfig: bootConfig[] = [
const opsBootConfig: bootConfig[] = [
{
english: 'STEP1',
label: '设备接入配置',
link: '/iot/link/accessConfig',
auth: menuPermission('link/accessConfig'),
link: 'link/AccessConfig',
image: '/images/home/guide-home4.png',
},
{
english: 'STEP2',
label: '日志排查',
link: '/iot/link/Log',
auth: menuPermission('link/Log'),
link: 'Log',
params: {
key: 'system',
},
image: '/images/home/guide-home5.png',
},
{
english: 'STEP3',
label: '实时监控',
link: '/iot/link/dashboard',
auth: menuPermission('link/dashboard'),
params: {
save: true,
},
link: 'link/DashBoard',
image: '/images/home/guide-home6.png',
},
];
const opsImages = [
'/images/home/guide-home4.png',
'/images/home/guide-home5.png',
'/images/home/guide-home6.png',
];
const opsBootConfig = _opsBootConfig.map((item, i) => ({
...item,
image: opsImages[i],
}));
// -
const opsStepDetails: recommendList[] = [
@ -194,42 +194,50 @@ const opsStepDetails: recommendList[] = [
details:
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
iconUrl: '/images/home/bottom-1.png',
linkUrl: '/iot/link/protocol',
auth: menuPermission('link/Protocol'),
linkUrl: 'link/Protocol',
},
{
title: '证书管理',
details: '统一维护平台内的证书,用于数据通信加密。',
iconUrl: '/images/home/bottom-6.png',
linkUrl: '/iot/link/Certificate',
auth: menuPermission('link/Certificate'),
linkUrl: 'link/Certificate',
},
{
title: '网络组件',
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
iconUrl: '/images/home/bottom-3.png',
linkUrl: '/iot/link/type',
auth: menuPermission('link/Type'),
linkUrl: 'link/Type',
},
{
title: '设备接入网关',
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
iconUrl: '/images/home/bottom-4.png',
linkUrl: '/iot/link/accessConfig',
auth: menuPermission('link/AccessConfig'),
linkUrl: 'link/AccessConfig',
},
{
title: '日志管理',
details: '监控系统日志,及时处理系统异常。',
iconUrl: '/images/home/bottom-5.png',
linkUrl: '/iot/link/Log',
auth: menuPermission('Log'),
linkUrl: 'Log',
params: {
key: 'system',
},
},
];
const productDialogVisible = ref(false);
const deviceDialogVisible = ref(false);
// ---- {save:true}
// ----- {id: 'xxxx', tab:'xxx'}
// ---- {save: true}
// ----
// ----
// -----
// ----
</script>

View File

@ -1,23 +1,23 @@
<template>
<div class="device-home-container">
<a-row :gutter="10">
<a-col :span="14">
<j-row :gutter="10">
<j-col :span="14">
<BootCard :cardData="opsBootConfig" cardTitle="运维引导" />
</a-col>
<a-col :span="10">
</j-col>
<j-col :span="10">
<BasicCountCard />
</a-col>
</a-row>
<a-row>
</j-col>
</j-row>
<j-row>
<PlatformPicCard />
</a-row>
<a-row>
</j-row>
<j-row>
<StepCard
cardTitle="运维管理推荐步骤"
tooltip="请根据业务需要对下述步骤进行选择性操作。"
:dataList="opsStepDetails"
/>
</a-row>
</j-row>
</div>
</template>
@ -26,24 +26,18 @@ import BootCard from '../BootCard.vue';
import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue';
import { useMenuStore } from "@/store/menu";
import { bootConfig, recommendList } from '../../typing';
//
const menuPermission = useMenuStore().hasPermission;
import type { bootConfig, recommendList } from '../../typing';
const opsBootConfig: bootConfig[] = [
{
english: 'STEP1',
label: '设备接入配置',
link: '/iot/link/accessConfig',
auth: menuPermission('link/accessConfig'),
link: 'link/AccessConfig',
},
{
english: 'STEP2',
label: '日志排查',
link: '/iot/link/Log',
auth: menuPermission('link/Log'),
link: 'Log',
params: {
key: 'system',
},
@ -51,10 +45,9 @@ const opsBootConfig: bootConfig[] = [
{
english: 'STEP3',
label: '实时监控',
link: '/iot/link/dashboard',
auth: menuPermission('link/dashboard'),
link: 'link/DashBoard',
params: {
save: true,
type: 'add',
},
},
];
@ -64,45 +57,36 @@ const opsStepDetails: recommendList[] = [
details:
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
iconUrl: '/images/home/bottom-1.png',
linkUrl: '/iot/link/protocol',
auth: menuPermission('link/Protocol'),
linkUrl: 'link/Protocol',
},
{
title: '证书管理',
details: '统一维护平台内的证书,用于数据通信加密。',
iconUrl: '/images/home/bottom-6.png',
linkUrl: '/iot/link/Certificate',
auth: menuPermission('link/Certificate'),
linkUrl: 'link/Certificate',
},
{
title: '网络组件',
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
iconUrl: '/images/home/bottom-3.png',
linkUrl: '/iot/link/type',
auth: menuPermission('link/Type'),
linkUrl: 'link/Type',
},
{
title: '设备接入网关',
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
iconUrl: '/images/home/bottom-4.png',
linkUrl: '/iot/link/accessConfig',
auth: menuPermission('link/AccessConfig'),
linkUrl: 'link/AccessConfig',
},
{
title: '日志管理',
details: '监控系统日志,及时处理系统异常。',
iconUrl: '/images/home/bottom-5.png',
linkUrl: '/iot/link/Log',
auth: menuPermission('Log'),
linkUrl: 'Log',
params: {
key: 'system',
}
},
},
];
</script>
<style lang="less" scoped>

View File

@ -26,13 +26,19 @@ const { jumpPage } = useMenuStore();
const projectNum = ref(0);
const deviceNum = ref(0);
const menuPermission = useMenuStore().hasPermission;
const getData = () => {
getDeviceCount_api().then((resp: any) => {
deviceNum.value = resp.result;
});
getProductCount_api({}).then((resp: any) => {
projectNum.value = resp.result;
});
//
menuPermission('device/Product') &&
getDeviceCount_api().then((resp: any) => {
deviceNum.value = resp.result;
});
//
menuPermission('device/Instance') &&
getProductCount_api({}).then((resp: any) => {
projectNum.value = resp.result;
});
};
getData();
</script>

View File

@ -1,27 +1,42 @@
<template>
<div class="device-home-container">
<a-row :gutter="24">
<a-col :span="14">
<j-row :gutter="24">
<j-col :span="14">
<BootCard :cardData="deviceBootConfig" cardTitle="物联网引导" />
</a-col>
<a-col :span="10">
</j-col>
<j-col :span="10">
<DeviceCountCard />
</a-col>
</a-row>
<a-row>
</j-col>
</j-row>
<j-row>
<PlatformPicCard />
</a-row>
<a-row>
</j-row>
<j-row>
<StepCard
cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="deviceStepDetails"
/>
</a-row>
</div>
</j-row>
<div class="dialog">
<ProductChooseDialog
v-if="productDialogVisible"
v-model:visible="productDialogVisible"
@confirm="(id:string)=>jumpPage('device/Product/Detail', { id, tab: 'Device'})"
/>
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id })"
/>
</div>
</div>
</template>
<script setup lang="ts" name="deviceHome">
import ProductChooseDialog from '../dialogs/ProductChooseDialog.vue';
import DeviceChooseDialog from '../dialogs/DeviceChooseDialog.vue';
import BootCard from '../BootCard.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
@ -29,6 +44,7 @@ import StepCard from '../StepCard.vue';
import { usePermissionStore } from '@/store/permission';
import { bootConfig, recommendList } from '../../typing';
import { useMenuStore } from '@/store/menu';
//
const hasPermission = usePermissionStore().hasPermission;
@ -39,6 +55,11 @@ const devicePermission = (action: string) =>
const rulePermission = (action: string) =>
hasPermission(`rule-engine/Instance:${action}`);
const { jumpPage } = useMenuStore();
const productDialogVisible = ref(false);
const deviceDialogVisible = ref(false);
const deviceBootConfig: bootConfig[] = [
{
english: 'STEP1',
@ -46,7 +67,7 @@ const deviceBootConfig: bootConfig[] = [
link: 'device/Product',
auth: productPermission('add'),
params: {
type: 'add',
save: true,
},
},
{
@ -64,7 +85,7 @@ const deviceBootConfig: bootConfig[] = [
link: 'rule-engine/Instance',
auth: rulePermission('add'),
params: {
type: 'add',
save: true,
},
},
];
@ -77,7 +98,7 @@ const deviceStepDetails: recommendList[] = [
linkUrl: 'device/Product',
auth: productPermission('add'),
params: {
type: 'add',
save: true,
},
},
{
@ -87,7 +108,9 @@ const deviceStepDetails: recommendList[] = [
iconUrl: '/images/home/bottom-1.png',
linkUrl: 'device/Product/Detail',
auth: productPermission('update'),
dialogTag: 'accessMethod',
onClick: () => {
productDialogVisible.value = true;
},
},
{
title: '添加测试设备',
@ -105,8 +128,9 @@ const deviceStepDetails: recommendList[] = [
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
iconUrl: '/images/home/bottom-2.png',
linkUrl: 'device/Instance/Detail',
auth: true,
dialogTag: 'funcTest',
onClick: () => {
deviceDialogVisible.value = true;
},
},
{
title: '批量添加设备',

View File

@ -3,34 +3,34 @@
<div class="title">请选择首页视图</div>
<div class="choose-view">
<a-row class="view-content" :gutter="24">
<a-col
<j-row class="view-content" :gutter="24">
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'device' }"
@click="selectValue = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</a-col>
<a-col
</j-col>
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'ops' }"
@click="selectValue = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</a-col>
<a-col
</j-col>
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'comprehensive' }"
@click="selectValue = 'comprehensive'"
>
<img :src="getImage('/home/comprehensive.png')" alt="" />
</a-col>
</a-row>
<a-button type="primary" class="btn" @click="confirm"
>确定</a-button
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm"
>确定</j-button
>
</div>
</div>

View File

@ -1,5 +1,5 @@
<template>
<dic class="step-container">
<div class="step-container">
<h5 class="title">
<span style="margin-right: 12px">{{ props.cardTitle }}</span>
<j-tooltip placement="top">
@ -19,27 +19,12 @@
<div class="box-details">{{ item.details }}</div>
</div>
</div>
<div class="dialogs">
<ProductChooseDialog
v-if="productDialogVisible"
v-model:visible="productDialogVisible"
@confirm="againJumpPage"
/>
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="againJumpPage"
/>
</div>
</dic>
</div>
</template>
<script setup lang="ts">
import { PropType } from 'vue';
import { message } from 'ant-design-vue';
import ProductChooseDialog from './dialogs/ProductChooseDialog.vue';
import DeviceChooseDialog from './dialogs/DeviceChooseDialog.vue';
import { recommendList } from '../typing';
import { useMenuStore } from '@/store/menu';
@ -51,33 +36,10 @@ const props = defineProps({
dataList: Array as PropType<recommendList[]>,
});
let selectRow: recommendList | rowType = {
params: {},
linkUrl: '',
};
//
const jumpPage = (row: recommendList) => {
if (!row.auth) return message.warning('暂无权限,请联系管理员');
selectRow = row; // 使
if (row.dialogTag == 'accessMethod')
return (productDialogVisible.value = true);
else if (row.dialogTag === 'funcTest')
return (deviceDialogVisible.value = true);
else if (row.linkUrl) {
_jumpPage(row.linkUrl, row.params);
}
};
//
const againJumpPage = (id: string) => {
_jumpPage(selectRow.linkUrl, { id });
};
const productDialogVisible = ref(false);
const deviceDialogVisible = ref(false);
type rowType = {
params: object;
linkUrl: string;
if (row.auth === false) return message.warning('暂无权限,请联系管理员');
row.onClick ? row.onClick(row) : _jumpPage(row.linkUrl, row.params);
};
</script>

View File

@ -17,12 +17,22 @@
<div class="card">
<h3 style="margin: 0 0 24px 0">基本信息</h3>
<p>
<span style="font-weight: bold">clientId: </span>
<span>{{ clientId }}</span>
<span class="label">clientId: </span>
<span class="value">{{ clientId }}</span>
</p>
<p>
<span style="font-weight: bold">secureKey:</span>
<span>{{ secureKey }}</span>
<span class="label">secureKey:</span>
<span class="value">
{{ showKey ? secureKey : '****************' }}
</span>
<AIcon
:type="
showKey
? 'EyeOutlined'
: 'EyeInvisibleOutlined'
"
@click="showKey = !showKey"
/>
</p>
</div>
</template>
@ -47,14 +57,15 @@ const currentView = ref<string>('');
const loading = ref<boolean>(true);
const clientId = useUserInfo().$state.userInfos.id;
const secureKey = ref<string>('');
const showKey = ref(false);
//
const setCurrentView = () => {
getView_api().then((resp: any) => {
if (resp.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result.username === 'admin') {
currentView.value = 'comprehensive';
if (resp.result) {
if (resp.result.username === 'admin')
currentView.value = 'comprehensive';
else currentView.value = resp.result?.content;
} else currentView.value = 'init';
}
});
@ -90,6 +101,15 @@ if (isNoCommunity) {
p {
margin: 0;
font-size: 16px;
.label {
font-weight: bold;
margin-right: 3px;
}
.value {
margin-right: 10px;
font-size: 14px;
}
}
}
}

View File

@ -4,9 +4,9 @@ export interface recommendList {
details: string;
iconUrl: string;
linkUrl: string;
params?: object;
auth: boolean;
dialogTag?: 'accessMethod' | 'funcTest';
params?: any;
auth?: boolean;
onClick?: Function
}
// 产品列表里的每项
export interface productItem {
@ -25,7 +25,7 @@ export interface bootConfig {
english: string,
label: string,
link: string,
auth: boolean,
auth?: boolean,
image?:string,
params?: object,
params?: any,
}

View File

@ -1,6 +1,6 @@
<!-- 国标级联-绑定通道 -->
<template>
<a-modal
<j-modal
v-model:visible="_vis"
title="绑定通道"
cancelText="取消"
@ -51,19 +51,19 @@
<h3>通道列表</h3>
</template>
<template #status="slotProps">
<a-space>
<a-badge
<j-space>
<j-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
</a-space>
></j-badge>
</j-space>
</template>
</JProTable>
</a-modal>
</j-modal>
</template>
<script setup lang="ts">

View File

@ -28,34 +28,34 @@
<h3>通道列表</h3>
</template>
<template #rightExtraRender>
<a-space>
<a-button type="primary" @click="bindVis = true">
<j-space>
<j-button type="primary" @click="bindVis = true">
绑定通道
</a-button>
<a-popconfirm
</j-button>
<j-popconfirm
title="确认解绑?"
@confirm="handleMultipleUnbind"
>
<a-button> 批量解绑 </a-button>
</a-popconfirm>
</a-space>
<j-button> 批量解绑 </j-button>
</j-popconfirm>
</j-space>
</template>
<template #gbChannelIdHeader="title">
<a-tooltip
<j-tooltip
title="国标级联有16位、20位两种格式。在当前页面修改不会修改视频设备-通道页面中的国标ID"
>
<a-space>
<j-space>
<span>{{ title }}</span>
<AIcon type="InfoCircleOutlined" />
</a-space>
</a-tooltip>
</j-space>
</j-tooltip>
</template>
<template #gbChannelId="slotProps">
<a-space>
<j-space>
<Ellipsis>
{{ slotProps.gbChannelId }}
</Ellipsis>
<a-popover
<j-popover
v-model:visible="slotProps.popVis"
trigger="click"
>
@ -70,7 +70,7 @@
</template>
<template #content>
<div class="simple-form">
<a-input
<j-input
v-model:value="gbID"
@change="validField(slotProps)"
/>
@ -82,67 +82,67 @@
该国标ID在同一设备下已存在
</div>
</div>
<a-button
<j-button
type="primary"
@click="handleSave(slotProps)"
:loading="loading"
style="width: 100%"
>
保存
</a-button>
</j-button>
</template>
<a-button type="link" @click="slotProps.popVis = true">
<j-button type="link" @click="slotProps.popVis = true">
<AIcon type="EditOutlined" />
</a-button>
</a-popover>
</a-space>
</j-button>
</j-popover>
</j-space>
</template>
<template #status="slotProps">
<a-space>
<a-badge
<j-space>
<j-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
</a-space>
></j-badge>
</j-space>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
<j-space :size="16">
<j-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
<j-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
<j-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
/></j-button>
</j-popconfirm>
<j-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
<j-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
/></j-button>
</j-button>
</j-tooltip>
</j-space>
</template>
</JProTable>

View File

@ -1,6 +1,6 @@
<!-- 国标级联-推送 -->
<template>
<a-modal
<j-modal
v-model:visible="_vis"
title="推送"
cancelText="取消"
@ -9,10 +9,10 @@
@ok="_vis = false"
@cancel="_vis = false"
>
<a-row :gutter="20">
<a-col :span="8">
<j-row :gutter="20">
<j-col :span="8">
<p>成功{{ successCount }}</p>
<a-space>
<j-space>
<p>失败{{ failCount }}</p>
<a
v-if="errMessage.length"
@ -24,19 +24,19 @@
"
>下载</a
>
</a-space>
</a-col>
<a-col :span="8">
</j-space>
</j-col>
<j-col :span="8">
<p>推送通道数量{{ data.count }}</p>
</a-col>
<a-col :span="8">
</j-col>
<j-col :span="8">
<p>已推送通道数量{{ successCount + failCount }}</p>
</a-col>
</a-row>
</j-col>
</j-row>
<div v-if="flag">
<a-textarea :rows="10" v-model:value="errStr" />
<j-textarea :rows="10" v-model:value="errStr" />
</div>
</a-modal>
</j-modal>
</template>
<script setup lang="ts">

View File

@ -1,14 +1,14 @@
<!-- 国标级联新增/编辑 -->
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="12">
<a-form ref="formRef" layout="vertical" :model="formData">
<a-row :gutter="24">
<j-card>
<j-row :gutter="24">
<j-col :span="12">
<j-form ref="formRef" layout="vertical" :model="formData">
<j-row :gutter="24">
<TitleComponent data="基本信息" />
<a-col :span="12">
<a-form-item
<j-col :span="12">
<j-form-item
label="名称"
name="cascadeName"
:rules="[
@ -22,14 +22,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.cascadeName"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="代理视频流"
name="proxyStream"
:rules="[
@ -39,23 +39,23 @@
},
]"
>
<a-radio-group
<j-radio-group
button-style="solid"
v-model:value="formData.proxyStream"
>
<a-radio-button :value="true">
<j-radio-button :value="true">
启用
</a-radio-button>
<a-radio-button :value="false">
</j-radio-button>
<j-radio-button :value="false">
禁用
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
</j-radio-button>
</j-radio-group>
</j-form-item>
</j-col>
<TitleComponent data="信令服务配置" />
<a-col :span="12">
<a-form-item
<j-col :span="12">
<j-form-item
name="clusterNodeId"
:rules="[
{
@ -67,25 +67,25 @@
<template #label>
<span>
集群节点
<a-tooltip
<j-tooltip
title="使用此集群节点级联到上级平台"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-select
<j-select
v-model:value="formData.clusterNodeId"
placeholder="请选择集群节点"
:options="clustersList"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="信令名称"
name="name"
:rules="[
@ -99,14 +99,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.name"
placeholder="请输入信令名称"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
label="上级SIP ID"
name="sipId"
:rules="[
@ -120,14 +120,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.sipId"
placeholder="请输入上级SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="上级SIP域"
name="domain"
:rules="[
@ -141,14 +141,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.domain"
placeholder="请输入上级平台SIP域"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="上级SIP 地址"
name="remoteAddress"
:rules="[
@ -161,17 +161,17 @@
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
<j-row :gutter="10">
<j-col :span="14">
<j-input
v-model:value="
formData.remoteAddress
"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input-number
</j-col>
<j-col :span="10">
<j-input-number
:min="1"
:max="65535"
v-model:value="
@ -180,13 +180,13 @@
placeholder="请输入端口"
style="width: 100%"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
</j-col>
</j-row>
</j-form-item>
</j-col>
<a-col :span="24">
<a-form-item
<j-col :span="24">
<j-form-item
label="本地SIP ID"
name="localSipId"
:rules="[
@ -200,14 +200,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.localSipId"
placeholder="网关侧的SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
name="host"
:rules="[
{
@ -222,36 +222,36 @@
<template #label>
<span>
SIP本地地址
<a-tooltip
<j-tooltip
title="使用指定的网卡和端口进行请求"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-row :gutter="10">
<a-col :span="14">
<a-select
<j-row :gutter="10">
<j-col :span="14">
<j-select
v-model:value="formData.host"
placeholder="请选择IP地址"
:options="allList"
/>
</a-col>
<a-col :span="10">
<a-select
</j-col>
<j-col :span="10">
<j-select
v-model:value="formData.port"
placeholder="请选择端口"
:options="allListPorts"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-col>
</j-row>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="SIP远程地址"
name="publicHost"
:rules="[
@ -264,17 +264,17 @@
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
<j-row :gutter="10">
<j-col :span="14">
<j-input
v-model:value="
formData.publicHost
"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input-number
</j-col>
<j-col :span="10">
<j-input-number
:min="1"
:max="65535"
v-model:value="
@ -283,12 +283,12 @@
placeholder="请输入端口"
style="width: 100%"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-col>
</j-row>
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
label="传输协议"
name="transport"
:rules="[
@ -298,22 +298,22 @@
},
]"
>
<a-radio-group
<j-radio-group
button-style="solid"
v-model:value="formData.transport"
@change="setPorts"
>
<a-radio-button value="UDP">
<j-radio-button value="UDP">
UDP
</a-radio-button>
<a-radio-button value="TCP">
</j-radio-button>
<j-radio-button value="TCP">
TCP
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-radio-button>
</j-radio-group>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="用户"
name="user"
:rules="[
@ -327,14 +327,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.user"
placeholder="请输入用户"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="接入密码"
name="password"
:rules="[
@ -348,14 +348,14 @@
},
]"
>
<a-input-password
<j-input-password
v-model:value="formData.password"
placeholder="请输入接入密码"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="厂商"
name="manufacturer"
:rules="[
@ -369,14 +369,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.manufacturer"
placeholder="请输入厂商"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="型号"
name="model"
:rules="[
@ -390,14 +390,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.model"
placeholder="请输入型号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="版本号"
name="firmware"
:rules="[
@ -411,14 +411,14 @@
},
]"
>
<a-input
<j-input
v-model:value="formData.firmware"
placeholder="请输入版本号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="心跳周期(秒)"
name="keepaliveInterval"
:rules="[
@ -428,7 +428,7 @@
},
]"
>
<a-input-number
<j-input-number
:min="1"
:max="10000"
v-model:value="
@ -437,10 +437,10 @@
placeholder="请输入心跳周期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="注册间隔(秒)"
name="registerInterval"
:rules="[
@ -450,7 +450,7 @@
},
]"
>
<a-input-number
<j-input-number
:min="1"
:max="10000"
v-model:value="
@ -459,29 +459,29 @@
placeholder="请输入注册间隔"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</j-form-item>
</j-col>
</j-row>
<a-form-item>
<a-button
<j-form-item>
<j-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
</j-button>
</j-form-item>
</j-form>
</j-col>
<j-col :span="12">
<div class="doc">
<h1>1.概述</h1>
<div>
配置国标级联平台可以将已经接入到自身的摄像头共享给第三方调用播放
</div>
<div>
<a-alert
<j-alert
message="注该配置只用于将本平台向上级联至第三方平台如需第三方平台向上级联至本平台请在“视频设备”页面新增设备时选择“GB/T28181”接入方式。"
type="info"
show-icon
@ -494,7 +494,7 @@
<h2>1上级SIP ID</h2>
<div>请填写第三方平台中配置的<b>SIP ID</b></div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/northbound/doc2.png')"
/>
@ -502,7 +502,7 @@
<h2>2上级SIP </h2>
<div>请填写第三方平台中配置的<b>SIP ID域</b></div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/northbound/doc1.png')"
/>
@ -510,7 +510,7 @@
<h2>3上级SIP 地址</h2>
<div>请填写第三方平台中配置的<b>SIP ID地址</b></div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/northbound/doc3.png')"
/>
@ -549,9 +549,9 @@
SIP代理与 SIP服务器出现1s误 差所经过的运行时间
</div>
</div>
</a-col>
</a-row>
</a-card>
</j-col>
</j-row>
</j-card>
</page-container>
</template>
@ -652,7 +652,7 @@ onMounted(() => {
});
const regDomain =
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/;
/[j-zA-Z0-9][-j-zA-Z0-9]{0,62}(\.[j-zA-Z0-9][-j-zA-Z0-9]{0,62})+\.?/;
/**
* 上级SIP地址 字段验证
* @param _

View File

@ -53,7 +53,7 @@
</h3>
<p>通道数量{{ slotProps.count }}</p>
<Ellipsis>
<a-badge
<j-badge
:text="`sip:${slotProps.sipConfigs[0]?.sipId}@${slotProps.sipConfigs[0]?.hostAndPort}`"
:status="
slotProps.status?.value === 'enabled'
@ -92,7 +92,7 @@
{{ slotProps.sipConfigs[0]?.publicHost }}
</template>
<template #status="slotProps">
<a-badge
<j-badge
:text="slotProps.status?.text"
:status="
slotProps.status?.value === 'enabled'
@ -102,7 +102,7 @@
/>
</template>
<template #onlineStatus="slotProps">
<a-badge
<j-badge
:text="slotProps.onlineStatus?.text"
:status="
slotProps.onlineStatus?.value === 'online'
@ -112,7 +112,7 @@
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<j-space :size="16">
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
@ -131,7 +131,7 @@
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</j-space>
</template>
</JProTable>

View File

@ -3,26 +3,26 @@
<div class="card-header">
<div class="title">{{ title }}</div>
<div class="tools">
<a-space>
<a-radio-group
<j-space>
<j-radio-group
v-model:value="dimension"
button-style="solid"
>
<a-radio-button value="today">今日</a-radio-button>
<a-radio-button value="week">近一周</a-radio-button>
<a-radio-button value="month">近一月</a-radio-button>
<a-radio-button value="year">近一年</a-radio-button>
</a-radio-group>
<a-range-picker
<j-radio-button value="today">今日</j-radio-button>
<j-radio-button value="week">近一周</j-radio-button>
<j-radio-button value="month">近一月</j-radio-button>
<j-radio-button value="year">近一年</j-radio-button>
</j-radio-group>
<j-range-picker
format="YYYY-MM-DD HH:mm:ss"
valueFormat="x"
v-model:value="dateRange"
/>
</a-space>
</j-space>
</div>
</div>
<div v-if="chartData.length" class="chart" ref="chartRef"></div>
<a-empty v-else class="no-data" description="暂无数据"></a-empty>
<j-empty v-else class="no-data" description="暂无数据"></j-empty>
</div>
</template>
@ -56,52 +56,49 @@ const chartRef = ref();
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(chartRef.value as HTMLElement);
const sData: number[] = props.chartData.map(
(m: any) => m.value && m.value.toFixed(2),
);
const maxY = Math.max.apply(null, sData.length ? sData : [0]);
const options = {
grid: {
left: '7%',
left: maxY > 100000 ? 90 : 50,
right: '5%',
top: '5%',
bottom: '5%',
},
tooltip: {
trigger: 'axis',
// formatter: '{a}<br>{b}: {c}',
axisPointer: {
type: 'shadow',
},
formatter: '{b0}<br />{a0}: {c0}',
},
xAxis: {
type: 'category',
data: props.chartData.map((m: any) => m.x),
},
yAxis: {
type: 'value',
// minInterval: 1,
},
xAxis: [
{
data: props.chartData.map((m: any) => m.x),
},
],
yAxis: [
{
show: false,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
splitLine: {
lineStyle: {
type: 'solid',
},
},
},
],
series: [
{
name: '播放数量(人次)',
data: sData,
type: 'bar',
barWidth: 16,
itemStyle: {
color: '#2f54eb',
},
},
{
name: '播放数量(人次)',
type: 'line',
symbol: 'circle',
showSymbol: false,
smooth: true,
data: props.chartData.map(
(m: any) => m.value && m.value.toFixed(2),
),
lineStyle: {
color: '#a5fff9',
},
data: sData,
},
],
};
@ -116,8 +113,6 @@ const createChart = () => {
watch(
() => props.chartData,
(val) => {
console.log('createChart', val);
createChart();
},
{ deep: true },
@ -138,22 +133,22 @@ watch(
() => dimension.value,
(val) => {
if (val === 'today') {
dateRange[0] = moment().startOf('day').format('x');
dateRange.value[0] = moment().startOf('day').format('x');
}
if (val === 'week') {
dateRange[0] = moment().subtract(1, 'week').format('x');
dateRange.value[0] = moment().subtract(1, 'week').format('x');
}
if (val === 'month') {
dateRange[0] = moment().subtract(1, 'month').format('x');
dateRange.value[0] = moment().subtract(1, 'month').format('x');
}
if (val === 'year') {
dateRange[0] = moment().subtract(1, 'year').format('x');
dateRange.value[0] = moment().subtract(1, 'year').format('x');
}
dateRange[1] = moment().format('x');
dateRange.value[1] = moment().format('x');
emits('change', {
time: {
start: dateRange[0],
end: dateRange[1],
start: dateRange.value[0],
end: dateRange.value[1],
},
});
},

View File

@ -4,12 +4,12 @@
<div class="content-left">
<div class="content-left-title">
<span>{{ title }}</span>
<a-tooltip placement="top" v-if="tooltip">
<j-tooltip placement="top" v-if="tooltip">
<template #title>
<span>{{ tooltip }}</span>
</template>
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</j-tooltip>
</div>
<div class="content-left-value">{{ value }}</div>
</div>
@ -20,7 +20,7 @@
<div class="top-card-footer">
<template v-for="(item, index) in footer" :key="index">
<span v-if="!item.status">{{ item.title }}</span>
<a-badge v-else :text="item.title" :status="item.status" />
<j-badge v-else :text="item.title" :status="item.status" />
<div class="footer-item-value">{{ item.value }}</div>
</template>
</div>

View File

@ -1,31 +1,31 @@
<template>
<page-container>
<a-row :gutter="24">
<a-col :span="6">
<j-row :gutter="24">
<j-col :span="6">
<TopCard
title="设备数量"
:img="getImage('/media/dashboard-1.png')"
:footer="deviceFooter"
:value="deviceTotal"
/>
</a-col>
<a-col :span="6">
</j-col>
<j-col :span="6">
<TopCard
title="通道数量"
:img="getImage('/media/dashboard-2.png')"
:footer="channelFooter"
:value="channelTotal"
/>
</a-col>
<a-col :span="6">
</j-col>
<j-col :span="6">
<TopCard
title="录像数量"
:img="getImage('/media/dashboard-3.png')"
:footer="aggFooter"
:value="aggTotal"
/>
</a-col>
<a-col :span="6">
</j-col>
<j-col :span="6">
<TopCard
title="播放中数量"
tooltip="当前正在播放的通道数量之和"
@ -33,15 +33,15 @@
:footer="aggPlayingFooter"
:value="aggPlayingTotal"
/>
</a-col>
<a-col :span="24" class="dash-board-bottom">
</j-col>
<j-col :span="24" class="dash-board-bottom">
<Card
title="播放数量(人次)"
:chartData="chartData"
@change="getPlayCount"
/>
</a-col>
</a-row>
</j-col>
</j-row>
</page-container>
</template>
@ -123,6 +123,7 @@ const getAggData = () => {
{
title: '总时长',
value: timestampFormat(res.result.duration),
status: '',
},
];
});
@ -139,6 +140,7 @@ const getAggPlayingData = () => {
{
title: '播放人数',
value: res.result.playerTotal,
status: '',
},
];
});
@ -188,9 +190,11 @@ const getPlayCount = async (params: any) => {
])
.then((res) => {
let result: any = [];
res.result.forEach((item: any) => {
result = [...result, ...item.data];
});
res.result
.sort((a: any, b: any) => b.data.timestamp - a.data.timestamp)
.forEach((item: any) => {
result.push({ group: item.group, ...item.data });
});
chartData.value = result.map((m: any) => ({
x: m.timeString,
value: m.value,

View File

@ -1,6 +1,6 @@
<!-- 视频设备 - 播放 -->
<template>
<a-modal
<j-modal
v-model:visible="_vis"
title="播放"
cancelText="取消"
@ -33,17 +33,17 @@
/>
</div>
<div class="media-live-tool">
<a-radio-group
<j-radio-group
v-model:value="mediaType"
button-style="solid"
@change="mediaStart"
>
<a-radio-button value="mp4">MP4</a-radio-button>
<a-radio-button value="flv">FLV</a-radio-button>
<a-radio-button value="m3u8">HLS</a-radio-button>
</a-radio-group>
<j-radio-button value="mp4">MP4</j-radio-button>
<j-radio-button value="flv">FLV</j-radio-button>
<j-radio-button value="m3u8">HLS</j-radio-button>
</j-radio-group>
</div>
</a-modal>
</j-modal>
</template>
<script setup lang="ts">

View File

@ -1,6 +1,6 @@
<!-- Modal 弹窗用于新增修改数据 -->
<template>
<a-modal
<j-modal
v-model:visible="_vis"
:title="!!formData.id ? '编辑' : '新增'"
width="650px"
@ -9,10 +9,10 @@
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formData" layout="vertical">
<a-row :gutter="10">
<a-col :span="12">
<a-form-item
<j-form ref="formRef" :model="formData" layout="vertical">
<j-row :gutter="10">
<j-col :span="12">
<j-form-item
name="channelId"
:rules="[
{
@ -26,22 +26,22 @@
>
<template #label>
通道ID
<a-tooltip title="若不填写系统将自动生成唯一ID">
<j-tooltip title="若不填写系统将自动生成唯一ID">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</template>
<a-input
<j-input
v-model:value="formData.channelId"
:disabled="!!formData.id"
placeholder="请输入通道ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
name="name"
label="通道名称"
:rules="[
@ -49,14 +49,29 @@
{ max: 64, message: '最多可输入64个字符' },
]"
>
<a-input
<j-input
v-model:value="formData.name"
placeholder="请输入通道名称"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24" v-if="route.query.type === 'gb28181-2016'">
<j-form-item
label="厂商"
name="manufacturer"
:rules="[
{ required: false, message: '' },
{ max: 64, message: '最多可输入64个字符' },
]"
>
<j-input
v-model:value="formData.manufacturer"
placeholder="请输入厂商名称"
/>
</j-form-item>
</j-col>
<j-col :span="24" v-if="route.query.type === 'fixed-media'">
<j-form-item
name="media_url"
:rules="[
{ required: true, message: '请输入视频地址' },
@ -65,66 +80,81 @@
>
<template #label>
视频地址
<a-tooltip
<j-tooltip
title="不同厂家的RTSP固定地址规则不同请按对应厂家的规则填写"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</template>
<a-input
<j-input
v-model:value="formData.media_url"
placeholder="请输入视频地址"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
name="media_username"
label="用户名"
:rules="{ max: 64, message: '最多可输入64个字符' }"
>
<a-input
<j-input
v-model:value="formData.media_username"
placeholder="请输入用户名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
name="media_password"
label="密码"
:rules="{ max: 64, message: '最多可输入64个字符' }"
>
<a-input-password
<j-input-password
v-model:value="formData.media_password"
placeholder="请输入密码"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item name="address" label="安装地址">
<a-input
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item name="address" label="安装地址">
<j-input
v-model:value="formData.address"
placeholder="请输入安装地址"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item name="description" label="说明">
<a-textarea
</j-form-item>
</j-col>
<j-col :span="24" v-if="route.query.type === 'gb28181-2016'">
<j-form-item label="云台类型" name="ptzType">
<j-select
v-model:value="formData.ptzType"
:options="[
{ label: '未知', value: 0 },
{ label: '球体', value: 1 },
{ label: '半球体', value: 2 },
{ label: '固定枪机', value: 3 },
{ label: '遥控枪机', value: 4 },
]"
placeholder="请选择云台类型"
/>
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item name="description" label="说明">
<j-textarea
v-model:value="formData.description"
:rows="4"
:maxlength="200"
showCount
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</j-form-item>
</j-col>
</j-row>
</j-form>
</j-modal>
</template>
<script setup lang="ts">
@ -163,7 +193,9 @@ const formData = ref({
description: '',
deviceId: route.query.id,
name: '',
// , others
manufacturer: '',
ptzType: '',
// , others
media_password: '',
media_url: '',
media_username: '',
@ -172,6 +204,7 @@ const formData = ref({
watch(
() => props.channelData,
(val: any) => {
console.log('val: ', val);
const {
id,
address,
@ -179,6 +212,8 @@ watch(
description,
deviceId,
name,
manufacturer,
ptzType,
others,
...extra
} = val;
@ -189,6 +224,8 @@ watch(
description,
deviceId,
name,
manufacturer,
ptzType: ptzType?.value || 0,
...others,
};
},
@ -225,6 +262,8 @@ const handleSubmit = () => {
media_url,
media_password,
media_username,
manufacturer,
ptzType,
...extraFormData
} = formData.value;
if (media_url || media_password || media_username) {

View File

@ -0,0 +1,96 @@
<template>
<div class="channel-tree">
<div class="channel-tree-query">
<j-input @change="queryTree" placeholder="请输入目录名称">
<template #suffix>
<AIcon type="SearchOutlined" />
</template>
</j-input>
</div>
<div class="channel-tree-content">
<j-tree
:height="500"
:selectedKeys="selectedKeys"
:treeData="treeData"
:onSelect="(keys:any) => {
if (keys.length) {
selectedKeys = keys
if (props.onSelect) {
props.onSelect(keys[0]);
}
}
}"
:fieldNames="{ key: 'id', title: 'name' }"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { debounce } from 'lodash';
import ChannelApi from '@/api/media/channel';
import DeviceApi from '@/api/media/device';
interface TreeProps {
deviceId: string;
onSelect: (id: string) => void;
onTreeLoad: (type: boolean) => void;
}
const props = defineProps<TreeProps>();
const treeData = ref<any[]>([]);
const selectedKeys = ref<string[]>([]);
const getTreeData = async (id: string, data?: any) => {
const { result } = await ChannelApi.queryTree(id, data);
treeData.value[0].children = result || [];
props.onTreeLoad(treeData.value[0].children.length > 10);
treeData.value = treeData.value;
};
/**
* 获取设备详情
* @param id
*/
const getDeviceDetail = async (id: string) => {
const deviceResp = await DeviceApi.detail(id);
if (deviceResp.status === 200) {
treeData.value = [
{
id,
name: deviceResp.result.name,
children: [],
},
];
selectedKeys.value = [id];
getTreeData(props.deviceId, {});
}
};
const queryTree = debounce((e: any) => {
getTreeData(props.deviceId, {
terms: [
{ column: 'name', termType: 'like', value: `%${e.target.value}%` },
],
});
}, 300);
watchEffect(() => {
getDeviceDetail(props.deviceId);
});
</script>
<style lang="less" scoped>
.channel-tree {
height: 100%;
.channel-tree-query {
margin-bottom: 16px;
}
.channel-tree-content {
min-height: calc(100% - 50px);
}
}
</style>

View File

@ -0,0 +1,48 @@
.device-channel-warp {
display: flex;
.left-warp {
position: relative;
margin-right: 16px;
padding: 20px;
background-color: #fff;
border-radius: 2px;
.left-content {
width: 0;
height: 100%;
overflow: hidden;
&.active {
width: 260px;
}
}
.left-warp--btn {
position: absolute;
top: 50%;
right: 0;
padding: 20px 4px;
color: rgba(#000, 0.3);
background-color: rgba(#f0f0f0, 6);
border-radius: ~'100% 0 0 100% / 50% 0 0 50%';
cursor: pointer;
&:hover {
color: rgba(#000, 0.5);
background-color: rgba(#f0f0f0, 8);
}
&.active {
right: 50%;
background-color: transparent;
border-radius: 0;
transform: translateX(50%) rotate(180deg);
}
}
}
.right {
flex: 1;
}
}

View File

@ -1,82 +1,102 @@
<!-- 视频设备-通道列表 -->
<template>
<page-container>
<j-advanced-search
type="simple"
:columns="columns"
target="product"
@search="handleSearch"
/>
<JProTable
ref="listRef"
:columns="columns"
:request="(e:any) => ChannelApi.list(e, route?.query.id as string)"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
}"
:params="params"
model="table"
>
<template #headerTitle>
<a-tooltip
v-if="route?.query.type === 'gb28181-2016'"
title="接入方式为GB/T28281时不支持新增"
<div class="device-channel-warp">
<div class="left-warp" v-if="route.query.type === 'gb28181-2016'">
<div class="left-content" :class="{ active: show }">
<Tree
:deviceId="deviceId"
:on-tree-load="(e) => (show = e)"
:on-select="handleSelect"
/>
</div>
<div
class="left-warp--btn"
:class="{ active: !show }"
@click="show = !show"
>
<a-button type="primary" disabled> 新增 </a-button>
</a-tooltip>
<a-button type="primary" @click="handleAdd" v-else>
新增
</a-button>
</template>
<template #status="slotProps">
<a-space>
<a-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
</a-space>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
<AIcon type="LeftOutlined" />
</div>
</div>
<div class="right">
<j-advanced-search
type="simple"
:columns="columns"
target="product"
@search="handleSearch"
/>
<JProTable
ref="listRef"
:columns="columns"
:request="(e:any) => ChannelApi.list(e, route?.query.id as string)"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
}"
:params="params"
model="table"
>
<template #headerTitle>
<j-tooltip
v-if="route?.query.type === 'gb28181-2016'"
title="接入方式为GB/T28281时不支持新增"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JProTable>
<j-button type="primary" disabled> 新增 </j-button>
</j-tooltip>
<j-button type="primary" @click="handleAdd" v-else>
新增
</j-button>
</template>
<template #status="slotProps">
<j-space>
<j-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></j-badge>
</j-space>
</template>
<template #action="slotProps">
<j-space :size="16">
<j-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<j-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<j-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></j-button>
</j-popconfirm>
<j-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<j-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></j-button>
</j-button>
</j-tooltip>
</j-space>
</template>
</JProTable>
</div>
</div>
<Save
v-model:visible="saveVis"
@ -94,7 +114,9 @@ import { useMenuStore } from 'store/menu';
import { message } from 'ant-design-vue';
import Save from './Save.vue';
import Live from './Live/index.vue';
import Tree from './Tree/index.vue';
import { cloneDeep } from 'lodash-es';
import { useElementSize } from '@vueuse/core';
const menuStory = useMenuStore();
const route = useRoute();
@ -253,4 +275,25 @@ const getActions = (
? actions.filter((f) => f.key !== 'delete')
: actions;
};
//
const show = ref(false);
const deviceId = computed(() => route.query.id as string);
const handleSelect = (key: string) => {
if (key === deviceId.value && listRef.value?.reload) {
listRef.value?.reload();
} else {
params.value = {
terms: [
{
column: 'parentId',
value: key,
},
],
};
}
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -53,14 +53,14 @@
/>
</div>
<div class="playback-right">
<a-spin :spinning="loading">
<a-tooltip placement="topLeft">
<j-spin :spinning="loading">
<j-tooltip placement="topLeft">
<template #title>
<div>云端存储在服务器中</div>
<div>本地存储在设备本地</div>
</template>
<div>类型: <AIcon type="QuestionCircleOutlined" /></div>
</a-tooltip>
</j-tooltip>
<RadioCard
layout="horizontal"
:options="[
@ -80,11 +80,11 @@
v-model="type"
/>
<div class="playback-calendar">
<a-calendar
<j-calendar
v-model:value="time"
:fullscreen="false"
:disabledDate="
(currentDate) => currentDate > dayjs(new Date())
(currentDate: Dayjs) => currentDate > dayjs(new Date())
"
@change="handlePanelChange"
/>
@ -93,20 +93,20 @@
class="playback-list"
:class="{ 'no-list': !historyList.length }"
>
<a-empty
<j-empty
v-if="!historyList.length"
description="暂无数据"
/>
<a-list
<j-list
v-else
class="playback-list-items"
itemLayout="horizontal"
:dataSource="historyList"
>
<template #renderItem="{ item }">
<a-list-item>
<j-list-item>
<template #actions>
<a-tooltip
<j-tooltip
key="play-btn"
:title="
(item.startTime ||
@ -136,8 +136,8 @@
"
/>
</a>
</a-tooltip>
<a-tooltip
</j-tooltip>
<j-tooltip
key="download"
:title="
type !== 'local'
@ -155,7 +155,7 @@
() => downloadClick(item)
"
/>
</a-tooltip>
</j-tooltip>
</template>
<div>
@ -173,12 +173,12 @@
).format('HH:mm:ss')
}}
</div>
</a-list-item>
</j-list-item>
</template>
<div></div>
</a-list>
</j-list>
</div>
</a-spin>
</j-spin>
</div>
</div>
</page-container>

View File

@ -1,5 +1,5 @@
<template>
<a-modal
<j-modal
v-model:visible="_vis"
title="快速添加"
cancelText="取消"
@ -9,38 +9,38 @@
:confirmLoading="btnLoading"
width="660px"
>
<a-form layout="vertical">
<a-form-item label="产品名称" v-bind="validateInfos.name">
<a-input
<j-form layout="vertical">
<j-form-item label="产品名称" v-bind="validateInfos.name">
<j-input
v-model:value="formData.name"
placeholder="请输入名称"
/>
</a-form-item>
</j-form-item>
<template v-if="channel === 'gb28181-2016' && formData.accessId">
<a-form-item
<j-form-item
label="接入密码"
v-bind="validateInfos['configuration.access_pwd']"
>
<a-input-password
<j-input-password
v-model:value="formData.configuration.access_pwd"
placeholder="请输入接入密码"
/>
</a-form-item>
<a-form-item label="流传输模式">
<a-select
</j-form-item>
<j-form-item label="流传输模式">
<j-select
v-model:value="formData.configuration.stream_mode"
placeholder="请选择流传输模式"
:options="streamMode"
/>
</a-form-item>
</j-form-item>
</template>
<a-form-item label="接入网关" v-bind="validateInfos.accessId">
<j-form-item label="接入网关" v-bind="validateInfos.accessId">
<div class="gateway-box">
<div v-if="!gatewayList.length">
暂无数据请先
<a-button type="link">
<j-button type="link">
添加{{ providerType[props.channel] }} 接入网关
</a-button>
</j-button>
</div>
<div
class="gateway-item"
@ -71,32 +71,34 @@
{{ item.name }}
</h3>
<div class="desc">{{ item.description }}</div>
<a-row v-if="props.channel === 'gb28181-2016'">
<a-col :span="12">
<j-row v-if="props.channel === 'gb28181-2016'">
<j-col :span="12">
{{ item.channelInfo?.name }}
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
{{ item.protocolDetail.name }}
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<p
v-for="(i, idx) in item.channelInfo
?.addresses"
:key="`${i.address}_address${idx}`"
>
<a-badge
:text="i.address"
:color="
i.health === -1
? 'red'
: 'green'
"
/>
<Ellipsis>
<j-badge
:text="i.address"
:color="
i.health === -1
? 'red'
: 'green'
"
/>
</Ellipsis>
</p>
</a-col>
</a-row>
<a-row v-else>
<a-col :span="24">
</j-col>
</j-row>
<j-row v-else>
<j-col :span="24">
<div class="subtitle">
{{ item.protocolDetail.name }}
</div>
@ -105,15 +107,15 @@
item.protocolDetail.description
}}
</p>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
</CardBox>
</div>
</div>
</a-form-item>
</a-form>
</a-modal>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts">
@ -263,6 +265,7 @@ const handleCancel = () => {
text-align: center;
.gateway-item {
padding: 16px;
text-align: left;
.card-item-content-title,
.desc,
.subtitle {

View File

@ -1,11 +1,11 @@
<!-- 视频设备新增/编辑 -->
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="12">
<a-form layout="vertical">
<a-form-item
<j-card>
<j-row :gutter="24">
<j-col :span="12">
<j-form layout="vertical">
<j-form-item
label="接入方式"
v-bind="validateInfos.channel"
>
@ -16,134 +16,134 @@
:disabled="!!route.query.id"
v-model="formData.channel"
/>
</a-form-item>
<a-row :gutter="24">
<a-col :span="8">
</j-form-item>
<j-row :gutter="24">
<j-col :span="8">
<JUpload
v-model:modelValue="formData.photoUrl"
:bgImage="formData.photoUrl"
/>
</a-col>
<a-col :span="16">
<a-form-item
</j-col>
<j-col :span="16">
<j-form-item
label="ID"
v-bind="validateInfos.id"
>
<a-input
<j-input
v-model:value="formData.id"
placeholder="请输入"
:disabled="!!route.query.id"
/>
</a-form-item>
<a-form-item
</j-form-item>
<j-form-item
label="设备名称"
v-bind="validateInfos.name"
>
<a-input
<j-input
v-model:value="formData.name"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
</j-form-item>
</j-col>
</j-row>
<j-form-item
label="所属产品"
v-bind="validateInfos.productId"
>
<a-row :gutter="[0, 10]">
<a-col :span="!!route.query.id ? 24 : 22">
<a-select
<j-row :gutter="[0, 10]">
<j-col :span="!!route.query.id ? 24 : 22">
<j-select
v-model:value="formData.productId"
placeholder="请选择所属产品"
:disabled="!!route.query.id"
>
<a-select-option
<j-select-option
v-for="(item, index) in productList"
:key="index"
:value="item.id"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-col>
<a-col :span="2" v-if="!route.query.id">
<a-button
</j-select-option>
</j-select>
</j-col>
<j-col :span="2" v-if="!route.query.id">
<j-button
type="link"
@click="saveProductVis = true"
>
<AIcon type="PlusOutlined" />
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item
</j-button>
</j-col>
</j-row>
</j-form-item>
<j-form-item
label="接入密码"
v-bind="validateInfos['others.access_pwd']"
v-if="formData.channel === 'gb28181-2016'"
>
<a-input-password
<j-input-password
v-model:value="formData.others.access_pwd"
placeholder="请输入接入密码"
/>
</a-form-item>
</j-form-item>
<template v-if="!!route.query.id">
<a-form-item
<j-form-item
label="流传输模式"
v-bind="validateInfos.streamMode"
>
<a-radio-group
<j-radio-group
button-style="solid"
v-model:value="formData.streamMode"
>
<a-radio-button value="UDP">
<j-radio-button value="UDP">
UDP
</a-radio-button>
<a-radio-button value="TCP_PASSIVE">
</j-radio-button>
<j-radio-button value="TCP_PASSIVE">
TCP被动
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="设备厂商">
<a-input
</j-radio-button>
</j-radio-group>
</j-form-item>
<j-form-item label="设备厂商">
<j-input
v-model:value="formData.manufacturer"
placeholder="请输入设备厂商"
/>
</a-form-item>
<a-form-item label="设备型号">
<a-input
</j-form-item>
<j-form-item label="设备型号">
<j-input
v-model:value="formData.model"
placeholder="请输入设备型号"
/>
</a-form-item>
<a-form-item label="固件版本">
<a-input
</j-form-item>
<j-form-item label="固件版本">
<j-input
v-model:value="formData.firmware"
placeholder="请输入固件版本"
/>
</a-form-item>
</j-form-item>
</template>
<a-form-item label="说明">
<a-textarea
<j-form-item label="说明">
<j-textarea
v-model:value="formData.description"
show-count
:maxlength="200"
:rows="5"
placeholder="请输入说明"
/>
</a-form-item>
<a-form-item>
<a-button
</j-form-item>
<j-form-item>
<j-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
</j-button>
</j-form-item>
</j-form>
</j-col>
<j-col :span="12">
<div v-if="1" class="doc" style="height: 800">
<h1>1.概述</h1>
<div>
@ -166,7 +166,7 @@
各个厂家不同设备型号的设备端配置页面布局存在差异但配置项基本大同小异此处以大华摄像头为例作为接入配置示例
</div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/media/doc1.png')"
/>
@ -177,7 +177,7 @@
SIP域通常为SIP服务器编号的前10位
</div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/media/doc2.png')"
/>
@ -187,7 +187,7 @@
SIP服务器IP/端口填入该设备所属产品-接入方式页面中连接信息的IP/端口
</div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/media/doc3.png')"
/>
@ -201,7 +201,7 @@
填入该设备所属产品-接入方式页面中GB28281配置处的接入密码
</div>
<div class="image">
<a-image
<j-image
width="100%"
:src="getImage('/media/doc4.png')"
/>
@ -230,9 +230,9 @@
只能选择接入方式为固定地址的产品若当前无对应产品可点击右侧快速添加按钮填写产品名称和选择固定地址类型的网关完成产品创建
</div>
</div>
</a-col>
</a-row>
</a-card>
</j-col>
</j-row>
</j-card>
<SaveProduct
v-model:visible="saveProductVis"
@ -285,7 +285,7 @@ const formRules = ref({
},
{ max: 64, message: '最多输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
pattern: /^[j-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
],

View File

@ -44,30 +44,30 @@
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">厂商</div>
<div>{{ slotProps.manufacturer }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
通道数量
</div>
<div>{{ slotProps.channelNumber }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">型号</div>
<div>{{ slotProps.model }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
接入方式
</div>
<div>
{{ providerType[slotProps.provider] }}
</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
@ -92,7 +92,7 @@
</CardBox>
</template>
<template #action="slotProps">
<a-space :size="16">
<j-space :size="16">
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
@ -111,7 +111,7 @@
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</j-space>
</template>
</JProTable>
</page-container>
@ -294,9 +294,12 @@ const getActions = (
data.state.value === 'notActive' ||
data.provider === 'fixed-media',
icon: 'SyncOutlined',
onClick: () => {
// updateChannel()
console.log('updateChannel: ', data);
onClick: async () => {
const res = await DeviceApi.updateChannels(data.id);
if (res.success) {
message.success('通道更新成功');
listRef.value?.reload();
}
},
},
{

View File

@ -1,14 +1,11 @@
<template>
<a-card class="device-count-container">
<template #title>
<h5 class="title">基础统计</h5>
</template>
<template #extra>
<span style="color: #1d39c4; cursor: pointer" @click="jumpPage"
>详情</span
>
</template>
<div class="device-count-container">
<h5 class="title">
<span>基础统计</span>
<a style="font-size: 12px;" @click="jumpPage">
详情
</a>
</h5>
<div class="box-list">
<div class="box-item">
<div class="label">设备数量</div>
@ -21,7 +18,7 @@
<img :src="getImage('/home/top-2.png')" alt="" />
</div>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
@ -56,6 +53,8 @@ const jumpPage = () => {
<style lang="less" scoped>
.device-count-container {
background-color: #fff;
padding: 24px 14px;
:deep(.ant-card-body) {
padding-top: 0;
}

View File

@ -1,26 +1,76 @@
<template>
<page-container>
<a-row :gutter="24">
<a-col :span="14">
<j-row :gutter="24">
<j-col :span="14">
<BootCard
:cardData="deviceBootConfig"
cardTitle="视频中心引导"
/>
</a-col>
<a-col :span="10">
</j-col>
<j-col :span="10">
<BasicCountCard />
</a-col>
<a-col :span="24" style="margin: 20px 0">
</j-col>
<j-col :span="24" style="margin: 20px 0">
<PlatformPicCard />
</a-col>
<a-col :span="24">
</j-col>
<j-col :span="24">
<StepCard
cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="deviceStepDetails"
/>
</a-col>
</a-row>
</j-col>
</j-row>
<!-- 选择设备 -->
<j-modal
title="选择设备"
width="800px"
v-model:visible="visible"
:maskClosable="false"
:destroyOnClose="true"
@cancel="visible = false"
@ok="handleSubmit"
>
<j-advanced-search
type="simple"
:columns="columns"
@search="handleSearch"
/>
<JProTable
ref="tableRef"
model="table"
rowKey="id"
:columns="columns"
:request="deviceApi.list"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:rowSelection="{
type: 'radio',
selectedRowKeys: deviceItem?.id
? [deviceItem.id]
: undefined,
onSelect: (record: any) => {
deviceItem = record;
}
}"
>
<template #state="slotProps">
<a-space>
<a-badge
:status="
slotProps.state.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.state.text"
/>
</a-space>
</template>
</JProTable>
</j-modal>
</page-container>
</template>
@ -33,6 +83,13 @@ import BasicCountCard from '@/views/media/Home/components/BasicCountCard.vue';
import { usePermissionStore } from '@/store/permission';
import type { bootConfig, recommendList } from '@/views/home/typing';
import deviceApi from '@/api/media/device';
import { message } from 'ant-design-vue';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
//
const hasPermission = usePermissionStore().hasPermission;
@ -40,59 +97,116 @@ const deviceBootConfig: bootConfig[] = [
{
english: 'STEP1',
label: '添加视频设备',
link: '/media/device/Save',
auth: hasPermission('/media/device/Save'),
params: {
save: true,
},
link: 'media/Device/Save',
auth: hasPermission('media/Device:add'),
},
{
english: 'STEP2',
label: '分屏展示',
link: '/media/SplitScreen',
auth: hasPermission('/media/SplitScreen'),
params: {
save: true,
},
link: 'media/SplitScreen',
},
{
english: 'STEP3',
label: '国标级联',
link: '/media/Cascade',
auth: hasPermission('/media/Cascade'),
params: {
save: true,
},
link: 'media/Cascade',
},
];
const deviceStepDetails: recommendList[] = [
{
title: '添加视频设备',
details: '根据视频设备的传输协议,在已创建的产品下添加对应的设备。',
iconUrl: '/images/home/bottom-6.png',
linkUrl: '/media/device/Save',
auth: hasPermission('/media/device/Save'),
params: {
save: true,
},
linkUrl: 'media/Device/Save',
auth: hasPermission('media/Device:add'),
},
{
title: '查看通道',
details: '查看设备下的通道数据,可以进行直播、录制等操作。',
iconUrl: '/images/home/bottom-7.png',
linkUrl: '/media/device/Channel',
auth: hasPermission('/media/device/Save'),
dialogTag: 'accessMethod',
// linkUrl: 'media/Device/Channel',
linkUrl: '',
auth: hasPermission('media/Device:view'),
onClick: (row: any) => {
if (hasPermission('media/Device:view')) {
visible.value = true;
} else {
message.warning('暂无权限,请联系管理员');
}
},
},
{
title: '分屏展示',
details: '对多个通道的视频流数据进行分屏展示。',
iconUrl: '/images/home/bottom-8.png',
linkUrl: '/media/SplitScreen',
auth: hasPermission('/media/SplitScreen'),
params: {
save: true,
linkUrl: 'media/SplitScreen',
},
];
//
const visible = ref(false);
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '通道数量',
dataIndex: 'channelNumber',
key: 'channelNumber',
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '在线', value: 'online' },
{ label: '离线', value: 'offline' },
],
handleValue: (v: any) => {
return v;
},
},
},
];
const params = ref<Record<string, any>>({});
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
};
const deviceItem = ref();
const handleSubmit = () => {
if (deviceItem.value && deviceItem.value.id) {
menuStory.jumpPage(
'media/Device/Channel',
{},
{
id: deviceItem.value.id,
type: deviceItem.value.provider,
},
);
} else {
message.warning('请选择设备');
}
};
</script>

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<a-card class="splitScreen">
<j-card class="splitScreen">
<div class="split-screen">
<LeftTree @onSelect="mediaStart" />
<div class="right-content">
@ -17,7 +17,7 @@
/>
</div>
</div>
</a-card>
</j-card>
</page-container>
</template>

View File

@ -1,6 +1,6 @@
<template>
<div class="left-content">
<a-tree
<j-tree
:height="700"
:show-line="{ showLeafIcon: false }"
:show-icon="true"
@ -15,7 +15,7 @@
v-if="!treeData.find((f: any) => f.id === id)"
/>
</template>
</a-tree>
</j-tree>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More