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/, pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'TimeTicker' styleDir: 'TimePicker'
}, },
{ {
pattern: /^Radio/, pattern: /^Radio/,

View File

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

View File

@ -1,7 +1,7 @@
import server from '@/utils/request' 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) 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 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 * @param productId id
@ -100,7 +100,7 @@ export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boo
* @param type * @param type
* @returns * @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是否重复 * 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) 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 * @param id id
@ -504,14 +520,14 @@ export const productCode = (productId: string) => server.get(`/device/transparen
* @param productId * @param productId
* @returns * @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 productId
* @param deviceId * @param deviceId
* @returns * @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 * @param productId
@ -520,13 +536,13 @@ export const deviceCode = (productId: string,deviceId:string) => server.get(`dev
* @param data * @param data
* @returns * @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 * @param data
* @returns * @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 * @param productId

View File

@ -16,6 +16,9 @@ export default {
// 删除 // 删除
del: (id: string) => server.remove(`/media/channel/${id}`), 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) => 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 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: { headers: {
'Content-Type': 'text/plain' '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 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: { headers: {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
} }

View File

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

View File

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

View File

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

View File

@ -2,36 +2,36 @@
<div class="indicator-box"> <div class="indicator-box">
<template v-if="['int', 'long', 'double', 'float'].includes(type)"> <template v-if="['int', 'long', 'double', 'float'].includes(type)">
<template v-if="value.range"> <template v-if="value.range">
<a-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small" <j-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></a-input-number> style="width: 100%;"></j-input-number>
~ ~
<a-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small" <j-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></a-input-number> style="width: 100%;"></j-input-number>
</template> </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>
<template v-else-if="type === 'date'"> <template v-else-if="type === 'date'">
<a-range-picker v-if="value.range" 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" />
<a-date-picker v-else 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>
<template v-else-if="type === 'boolean'"> <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>
<template v-else-if="type === 'string'"> <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>
<template v-else> <template v-else>
<template v-if="value.range"> <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> </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> </template>
<div v-if="type !== 'boolean' && type !== 'string'"> <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>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
src/global.d.ts vendored
View File

@ -12,3 +12,4 @@ declare module '*.js';
declare module '*.ts'; declare module '*.ts';
declare module 'js-cookie'; 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', path: '/form',
component: () => import('@/views/demo/Form.vue') component: () => import('@/views/demo/Form.vue')
}, },
{ // {
path: '/system/Api', // path: '/system/Api',
component: () => import('@/views/system/Platforms/index.vue') // component: () => import('@/views/system/Platforms/index.vue')
}, // },
// end: 测试用, 可删除 // end: 测试用, 可删除
// 初始化 // 初始化

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@
<j-form-item name="messageType" label="指令类型" :rules="{ <j-form-item name="messageType" label="指令类型" :rules="{
required: true, required: true,
message: '请选择指令类型', message: '请选择指令类型',
}"> }" class="other">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption"> <j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search>
<j-select-option value="READ_PROPERTY">读取属性</j-select-option> <j-select-option value="READ_PROPERTY">读取属性</j-select-option>
<j-select-option value="WRITE_PROPERTY">修改属性</j-select-option> <j-select-option value="WRITE_PROPERTY">修改属性</j-select-option>
<j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option> <j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option>
@ -22,7 +22,7 @@
required: true, required: true,
message: '请选择属性', 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-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select> </j-select>
</j-form-item> </j-form-item>
@ -32,7 +32,27 @@
required: true, required: true,
message: '请输入值', 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-form-item>
</j-col> </j-col>
<j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'"> <j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
@ -40,7 +60,7 @@
required: true, required: true,
message: '请选择功能', 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-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select> </j-select>
</j-form-item> </j-form-item>
@ -62,10 +82,6 @@ import EditTable from './EditTable.vue'
const formRef = ref(); const formRef = ref();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const props = defineProps({ const props = defineProps({
actionType: { actionType: {
type: String, type: String,
@ -89,10 +105,12 @@ const props = defineProps({
type Emits = { type Emits = {
(e: 'update:modelValue', data: any): void; (e: 'update:modelValue', data: any): void;
}; };
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const modelRef = computed({ const modelRef = computed({
get: () => { get: () => {
onPropertyChange(props.modelValue?.message?.properties)
return props.modelValue || { return props.modelValue || {
messageType: undefined, messageType: undefined,
message: { message: {
@ -107,6 +125,8 @@ const modelRef = computed({
} }
}) })
const property = ref<any>({})
const funcChange = (val: string) => { const funcChange = (val: string) => {
if(val){ if(val){
const arr = props.metadata?.functions.find((item: any) => item.id === val)?.inputs || [] 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) => { const saveBtn = () => new Promise((resolve) => {
formRef.value.validate() formRef.value.validate()
.then(() => { .then(() => {
@ -140,3 +167,12 @@ const saveBtn = () => new Promise((resolve) => {
defineExpose({ saveBtn }) 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="请选择产品" placeholder="请选择产品"
v-model:value="modelRef.id" v-model:value="modelRef.id"
show-search show-search
:filter-option="filterOption"
@change="productChange" @change="productChange"
> >
<j-select-option <j-select-option
@ -89,7 +88,6 @@
placeholder="请选择设备类型" placeholder="请选择设备类型"
v-model:value="modelRef.applianceType" v-model:value="modelRef.applianceType"
show-search show-search
:filter-option="filterOption"
@change="typeChange" @change="typeChange"
> >
<j-select-option <j-select-option
@ -170,13 +168,10 @@
item.action item.action
" "
show-search show-search
:filter-option="
filterOption
"
> >
<j-select-option <j-select-option
v-for="i in getTypesActions( v-for="i in getTypesActions(
item.action, item.action || ''
)" )"
:key="i.id" :key="i.id"
:value="i.id" :value="i.id"
@ -218,9 +213,6 @@
item.actionType item.actionType
" "
show-search show-search
:filter-option="
filterOption
"
> >
<j-select-option <j-select-option
value="command" value="command"
@ -261,6 +253,9 @@
</j-row> </j-row>
</j-collapse-panel> </j-collapse-panel>
</j-collapse> </j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col> </j-col>
<j-col :span="24"> <j-col :span="24">
<j-button <j-button
@ -323,13 +318,10 @@
item.source item.source
" "
show-search show-search
:filter-option="
filterOption
"
> >
<j-select-option <j-select-option
v-for="i in getDuerOSProperties( v-for="i in getDuerOSProperties(
item.source, item.source || '',
)" )"
:key="i.id" :key="i.id"
:value="i.id" :value="i.id"
@ -361,16 +353,13 @@
" "
mode="tags" mode="tags"
show-search show-search
:filter-option="
filterOption
"
> >
<j-select-option <j-select-option
v-for="i in getProductProperties( v-for="i in getProductProperties(
item.target, item.target,
)" )"
:key="i.id" :key="i.id"
:value="item.id" :value="i.id"
>{{ >{{
i.name i.name
}}</j-select-option }}</j-select-option
@ -381,6 +370,9 @@
</j-row> </j-row>
</j-collapse-panel> </j-collapse-panel>
</j-collapse> </j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col> </j-col>
<j-col :span="24"> <j-col :span="24">
<j-button <j-button
@ -494,10 +486,6 @@ const onActionCollChange = (_key: string[]) => {
actionActiveKey.value = _key; actionActiveKey.value = _key;
}; };
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const addItem = () => { const addItem = () => {
actionActiveKey.value.push(String(modelRef.actionMappings.length)); actionActiveKey.value.push(String(modelRef.actionMappings.length));
modelRef.actionMappings.push({ modelRef.actionMappings.push({
@ -636,8 +624,9 @@ const saveBtn = async () => {
.then(async (data: any) => { .then(async (data: any) => {
if (tasks.every((item) => item) && data) { if (tasks.every((item) => item) && data) {
loading.value = true; loading.value = true;
const resp = await savePatch(data); const resp = await savePatch(data).finally(() => {
loading.value = false; loading.value = false;
})
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
formRef.value.resetFields(); formRef.value.resetFields();

View File

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

View File

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

View File

@ -8,10 +8,15 @@
style="width: 350px; justify-content: center" style="width: 350px; justify-content: center"
> >
<img <img
v-if="userInfo.avatar"
:src="userInfo.avatar" :src="userInfo.avatar"
style="width: 140px; border-radius: 70px" style="width: 140px; border-radius: 70px"
alt="" alt=""
/> />
<div class="default-avatar" v-else>
<AIcon type="UserOutlined" />
</div>
<div <div
style=" style="
width: 100%; width: 100%;
@ -29,6 +34,7 @@
}" }"
:action="`${BASE_API_PATH}/file/static`" :action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload" @change="upload.changeBackUpload"
:beforeUpload="upload.beforeUpload"
> >
<j-button> <j-button>
<AIcon type="UploadOutlined" /> <AIcon type="UploadOutlined" />
@ -51,11 +57,17 @@
</div> </div>
<div class="info-card"> <div class="info-card">
<p>注册时间</p> <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>
<div class="info-card"> <div class="info-card">
<p>电话</p> <p>电话</p>
<p>{{ userInfo.telephone }}</p> <p>{{ userInfo.telephone || '-' }}</p>
</div> </div>
<div class="info-card"> <div class="info-card">
<p>姓名</p> <p>姓名</p>
@ -117,7 +129,7 @@
type="link" type="link"
@click="editPasswordVisible = true" @click="editPasswordVisible = true"
> >
<AIcon type="EditOutlined" style="color: #1d39c4;" /> <AIcon type="EditOutlined" style="color: #1d39c4" />
</PermissionButton> </PermissionButton>
</span> </span>
</div> </div>
@ -205,7 +217,7 @@
<EditInfoDialog <EditInfoDialog
v-if="editInfoVisible" v-if="editInfoVisible"
v-model:visible="editInfoVisible" v-model:visible="editInfoVisible"
:data="{...userInfo}" :data="{ ...userInfo }"
@ok="getUserInfo" @ok="getUserInfo"
/> />
<EditPasswordDialog <EditPasswordDialog
@ -277,6 +289,15 @@ const upload = reactive({
message.error('logo上传失败请稍后再试'); 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>(); const isApiUser = ref<boolean>();
@ -346,7 +367,7 @@ function getViews() {
background-color: #f0f2f5; background-color: #f0f2f5;
min-height: 100vh; min-height: 100vh;
.card { .card {
margin: 24px; margin: 16px 0;
padding: 24px; padding: 24px;
background-color: #fff; background-color: #fff;
position: relative; position: relative;
@ -370,6 +391,18 @@ function getViews() {
flex-wrap: wrap; flex-wrap: wrap;
.content-item { .content-item {
margin-right: 24px; 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 { .info-card {
width: 25%; width: 25%;

View File

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

View File

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

View File

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

View File

@ -1,19 +1,46 @@
<template> <template>
<div style="width: 100%; height: 400px"> <div style="width: 100%; height: 400px">
<el-amap <AmapComponent>
> <el-amap-label-marker
</el-amap> 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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { initAMapApiLoader } from '@vuemap/vue-amap'; import AmapComponent from '@/components/AMapComponent/index.vue';
import '@vuemap/vue-amap/dist/style.css'; import { getGo } from '@/api/device/dashboard';
initAMapApiLoader({ let point = ref();
// key: '95fa72137f4263f8e64ae01f766ad09c', const getMapData = async () => {
key: 'a0415acfc35af15f10221bfa5a6850b4', const res = await getGo({});
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0', point.value = res.result?.features;
}); };
getMapData();
</script> </script>
<style scoped> <style scoped>
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -306,23 +306,6 @@ const columns = [
const _selectedRowKeys = ref<string[]>([]); const _selectedRowKeys = ref<string[]>([]);
const currentForm = ref({}); 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 = ( const getActions = (
data: Partial<Record<string, any>>, data: Partial<Record<string, any>>,
type: 'card' | 'table', type: 'card' | 'table',

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<template> <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"> @close="close" destroy-on-close :z-index="1000" placement="right">
<template #extra> <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> </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> <BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form> </j-form>
</a-drawer> </j-drawer>
</template> </template>
<script lang="ts" setup name="Edit"> <script lang="ts" setup name="Edit">
import { useInstanceStore } from '@/store/instance'; import { useInstanceStore } from '@/store/instance';
@ -22,6 +22,7 @@ import { DeviceInstance } from '@/views/device/Instance/typings';
import BaseForm from './BaseForm.vue'; import BaseForm from './BaseForm.vue';
import { PropType } from 'vue'; import { PropType } from 'vue';
import { _deploy } from '@/api/device/product'; import { _deploy } from '@/api/device/product';
import { cloneDeep } from 'lodash';
const props = defineProps({ const props = defineProps({
type: { type: {
@ -60,7 +61,7 @@ const form = reactive({
model: {} as any, model: {} as any,
}) })
if (metadataStore.model.action === 'edit') { if (metadataStore.model.action === 'edit') {
form.model = metadataStore.model.item form.model = cloneDeep(metadataStore.model.item)
} }
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@ -75,7 +76,9 @@ const save = reactive({
const type = metadataStore.model.type const type = metadataStore.model.type
const _detail: ProductItem | DeviceInstance = props.type === 'device' ? instanceStore.detail : productStore.current const _detail: ProductItem | DeviceInstance = props.type === 'device' ? instanceStore.detail : productStore.current
const _metadata = JSON.parse(_detail?.metadata || '{}') 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 (formValue.id) {
if (metadataStore.model.action === 'add' && list.some(item => item.id === formValue.id)) { if (metadataStore.model.action === 'add' && list.some(item => item.id === formValue.id)) {
message.error('标识已存在') 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> <template>
<j-pro-table :loading="loading" :data-source="data" size="small" :columns="columns" row-key="id" model="TABLE"> <div class="table-header">
<template #headerTitle> <div>
<a-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></a-input-search> <j-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></j-input-search>
</template> </div>
<template #rightExtraRender> <div>
<PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick" <PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick"
:disabled="operateLimits('add', type)" :tooltip="{ :disabled="operateLimits('add', type)" :tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增', title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
@ -14,45 +14,50 @@
新增 新增
</PermissionButton> </PermissionButton>
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type" @refresh="refreshMetadata"></Edit> <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>
<template #level="slotProps"> </a-table>
{{ 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>
</template> </template>
<script setup lang="ts" name="BaseMetadata"> <script setup lang="ts" name="BaseMetadata">
import type { MetadataItem, MetadataType } from '@/views/device/Product/typings' import type { MetadataItem, MetadataType } from '@/views/device/Product/typings'
@ -61,14 +66,10 @@ import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product' import { useProductStore } from '@/store/product'
import { useMetadataStore } from '@/store/metadata' import { useMetadataStore } from '@/store/metadata'
import PermissionButton from '@/components/PermissionButton/index.vue' import PermissionButton from '@/components/PermissionButton/index.vue'
import { message } from 'ant-design-vue/es' import { TablePaginationConfig, message } from 'ant-design-vue/es'
import { SystemConst } from '@/utils/consts'
import { Store } from 'jetlinks-store'
import { asyncUpdateMetadata, removeMetadata } from '../metadata' import { asyncUpdateMetadata, removeMetadata } from '../metadata'
import { detail } from '@/api/device/instance' import { detail } from '@/api/device/instance'
import Edit from './Edit/index.vue' import Edit from './Edit/index.vue'
// import { detail } from '@/api/device/instance'
// import { detail as productDetail } from '@/api/device/product'
interface Props { interface Props {
type: MetadataType; type: MetadataType;
target: 'product' | 'device'; target: 'product' | 'device';
@ -106,6 +107,15 @@ const actions = [
scopedSlots: true, 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 columns = computed(() => MetadataMapping.get(type)!.concat(actions))
const items = computed(() => JSON.parse((target === 'product' ? productStore.current?.metadata : instanceStore.current.metadata) || '{}') as MetadataItem[]) const items = computed(() => JSON.parse((target === 'product' ? productStore.current?.metadata : instanceStore.current.metadata) || '{}') as MetadataItem[])
const searchValue = ref<string>() const searchValue = ref<string>()
@ -118,10 +128,6 @@ const handleSearch = (searchValue: string) => {
} }
} }
onMounted(() => {
})
const refreshMetadata = () => { const refreshMetadata = () => {
loading.value = true loading.value = true
// const res = target === 'product' // const res = target === 'product'
@ -135,6 +141,13 @@ const refreshMetadata = () => {
watch([route.params.id, type], refreshMetadata, { immediate: true }) watch([route.params.id, type], refreshMetadata, { immediate: true })
const metadataStore = useMetadataStore() const metadataStore = useMetadataStore()
watch(() => metadataStore.model.importMetadata,
(val: boolean) => {
if (!!val) {
refreshMetadata()
metadataStore.set('importMetadata', false)
}
})
const handleAddClick = () => { const handleAddClick = () => {
metadataStore.set('edit', true) metadataStore.set('edit', true)
metadataStore.set('item', undefined) metadataStore.set('item', undefined)
@ -165,11 +178,18 @@ const handleEditClick = (record: MetadataItem) => {
} }
const resetMetadata = async () => { 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 { id } = route.params
const resp = await detail(id as string); if (target === 'device') {
if (resp.status === 200) { instanceStore.refresh(id as string)
instanceStore.setCurrent(resp?.result || []); } else {
productStore.refresh(id as string)
} }
metadataStore.set('importMetadata', true)
}; };
const removeItem = async (record: MetadataItem) => { const removeItem = async (record: MetadataItem) => {
@ -180,7 +200,7 @@ const removeItem = async (record: MetadataItem) => {
const result = await asyncUpdateMetadata(target, _currentData); const result = await asyncUpdateMetadata(target, _currentData);
if (result.status === 200) { if (result.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
Store.set(SystemConst.REFRESH_METADATA_TABLE, true); // Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
metadataStore.model.edit = false; metadataStore.model.edit = false;
metadataStore.model.item = {}; metadataStore.model.item = {};
resetMetadata(); resetMetadata();
@ -190,5 +210,9 @@ const removeItem = async (record: MetadataItem) => {
}; };
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.table-header {
display: flex;
justify-content: space-between;
padding: 16px 0;
}
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

@ -28,34 +28,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue'; 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({ const props = defineProps({
cardData: Array<bootConfig>, cardData: Array<bootConfig>,
cardTitle: String, cardTitle: String,
}); });
const { cardData, cardTitle } = toRefs(props); const { cardData, cardTitle } = toRefs(props);
const { jumpPage: _jumpPage } = useMenuStore();
const jumpPage = (row: bootConfig): void => { const jumpPage = (item: bootConfig) => {
if (row.auth && row.link) { if (item.auth === undefined || item.auth) _jumpPage(item.link, item.params);
router.push(`${row.link}${objToParams(row.params || {})}`); else message.warning('暂无权限,请联系管理员');
} 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 '';
}; };
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,12 +17,22 @@
<div class="card"> <div class="card">
<h3 style="margin: 0 0 24px 0">基本信息</h3> <h3 style="margin: 0 0 24px 0">基本信息</h3>
<p> <p>
<span style="font-weight: bold">clientId: </span> <span class="label">clientId: </span>
<span>{{ clientId }}</span> <span class="value">{{ clientId }}</span>
</p> </p>
<p> <p>
<span style="font-weight: bold">secureKey:</span> <span class="label">secureKey:</span>
<span>{{ secureKey }}</span> <span class="value">
{{ showKey ? secureKey : '****************' }}
</span>
<AIcon
:type="
showKey
? 'EyeOutlined'
: 'EyeInvisibleOutlined'
"
@click="showKey = !showKey"
/>
</p> </p>
</div> </div>
</template> </template>
@ -47,14 +57,15 @@ const currentView = ref<string>('');
const loading = ref<boolean>(true); const loading = ref<boolean>(true);
const clientId = useUserInfo().$state.userInfos.id; const clientId = useUserInfo().$state.userInfos.id;
const secureKey = ref<string>(''); const secureKey = ref<string>('');
const showKey = ref(false);
// //
const setCurrentView = () => { const setCurrentView = () => {
getView_api().then((resp: any) => { getView_api().then((resp: any) => {
if (resp.status === 200) { if (resp.status === 200) {
if (resp.result) currentView.value = resp.result?.content; if (resp.result) {
else if (resp.result.username === 'admin') { if (resp.result.username === 'admin')
currentView.value = 'comprehensive'; currentView.value = 'comprehensive';
else currentView.value = resp.result?.content;
} else currentView.value = 'init'; } else currentView.value = 'init';
} }
}); });
@ -90,6 +101,15 @@ if (isNoCommunity) {
p { p {
margin: 0; margin: 0;
font-size: 16px; 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; details: string;
iconUrl: string; iconUrl: string;
linkUrl: string; linkUrl: string;
params?: object; params?: any;
auth: boolean; auth?: boolean;
dialogTag?: 'accessMethod' | 'funcTest'; onClick?: Function
} }
// 产品列表里的每项 // 产品列表里的每项
export interface productItem { export interface productItem {
@ -25,7 +25,7 @@ export interface bootConfig {
english: string, english: string,
label: string, label: string,
link: string, link: string,
auth: boolean, auth?: boolean,
image?:string, image?:string,
params?: object, params?: any,
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<!-- Modal 弹窗用于新增修改数据 --> <!-- Modal 弹窗用于新增修改数据 -->
<template> <template>
<a-modal <j-modal
v-model:visible="_vis" v-model:visible="_vis"
:title="!!formData.id ? '编辑' : '新增'" :title="!!formData.id ? '编辑' : '新增'"
width="650px" width="650px"
@ -9,10 +9,10 @@
@ok="handleSubmit" @ok="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
> >
<a-form ref="formRef" :model="formData" layout="vertical"> <j-form ref="formRef" :model="formData" layout="vertical">
<a-row :gutter="10"> <j-row :gutter="10">
<a-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
name="channelId" name="channelId"
:rules="[ :rules="[
{ {
@ -26,22 +26,22 @@
> >
<template #label> <template #label>
通道ID 通道ID
<a-tooltip title="若不填写系统将自动生成唯一ID"> <j-tooltip title="若不填写系统将自动生成唯一ID">
<AIcon <AIcon
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
style="margin-left: 2px" style="margin-left: 2px"
/> />
</a-tooltip> </j-tooltip>
</template> </template>
<a-input <j-input
v-model:value="formData.channelId" v-model:value="formData.channelId"
:disabled="!!formData.id" :disabled="!!formData.id"
placeholder="请输入通道ID" placeholder="请输入通道ID"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
name="name" name="name"
label="通道名称" label="通道名称"
:rules="[ :rules="[
@ -49,14 +49,29 @@
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
]" ]"
> >
<a-input <j-input
v-model:value="formData.name" v-model:value="formData.name"
placeholder="请输入通道名称" placeholder="请输入通道名称"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="24"> <j-col :span="24" v-if="route.query.type === 'gb28181-2016'">
<a-form-item <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" name="media_url"
:rules="[ :rules="[
{ required: true, message: '请输入视频地址' }, { required: true, message: '请输入视频地址' },
@ -65,66 +80,81 @@
> >
<template #label> <template #label>
视频地址 视频地址
<a-tooltip <j-tooltip
title="不同厂家的RTSP固定地址规则不同请按对应厂家的规则填写" title="不同厂家的RTSP固定地址规则不同请按对应厂家的规则填写"
> >
<AIcon <AIcon
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
style="margin-left: 2px" style="margin-left: 2px"
/> />
</a-tooltip> </j-tooltip>
</template> </template>
<a-input <j-input
v-model:value="formData.media_url" v-model:value="formData.media_url"
placeholder="请输入视频地址" placeholder="请输入视频地址"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
name="media_username" name="media_username"
label="用户名" label="用户名"
:rules="{ max: 64, message: '最多可输入64个字符' }" :rules="{ max: 64, message: '最多可输入64个字符' }"
> >
<a-input <j-input
v-model:value="formData.media_username" v-model:value="formData.media_username"
placeholder="请输入用户名" placeholder="请输入用户名"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
name="media_password" name="media_password"
label="密码" label="密码"
:rules="{ max: 64, message: '最多可输入64个字符' }" :rules="{ max: 64, message: '最多可输入64个字符' }"
> >
<a-input-password <j-input-password
v-model:value="formData.media_password" v-model:value="formData.media_password"
placeholder="请输入密码" placeholder="请输入密码"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="24"> <j-col :span="24">
<a-form-item name="address" label="安装地址"> <j-form-item name="address" label="安装地址">
<a-input <j-input
v-model:value="formData.address" v-model:value="formData.address"
placeholder="请输入安装地址" placeholder="请输入安装地址"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="24"> <j-col :span="24" v-if="route.query.type === 'gb28181-2016'">
<a-form-item name="description" label="说明"> <j-form-item label="云台类型" name="ptzType">
<a-textarea <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" v-model:value="formData.description"
:rows="4" :rows="4"
:maxlength="200" :maxlength="200"
showCount showCount
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
</a-form> </j-form>
</a-modal> </j-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -163,7 +193,9 @@ const formData = ref({
description: '', description: '',
deviceId: route.query.id, deviceId: route.query.id,
name: '', name: '',
// , others manufacturer: '',
ptzType: '',
// , others
media_password: '', media_password: '',
media_url: '', media_url: '',
media_username: '', media_username: '',
@ -172,6 +204,7 @@ const formData = ref({
watch( watch(
() => props.channelData, () => props.channelData,
(val: any) => { (val: any) => {
console.log('val: ', val);
const { const {
id, id,
address, address,
@ -179,6 +212,8 @@ watch(
description, description,
deviceId, deviceId,
name, name,
manufacturer,
ptzType,
others, others,
...extra ...extra
} = val; } = val;
@ -189,6 +224,8 @@ watch(
description, description,
deviceId, deviceId,
name, name,
manufacturer,
ptzType: ptzType?.value || 0,
...others, ...others,
}; };
}, },
@ -225,6 +262,8 @@ const handleSubmit = () => {
media_url, media_url,
media_password, media_password,
media_username, media_username,
manufacturer,
ptzType,
...extraFormData ...extraFormData
} = formData.value; } = formData.value;
if (media_url || media_password || media_username) { 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> <template>
<page-container> <page-container>
<j-advanced-search <div class="device-channel-warp">
type="simple" <div class="left-warp" v-if="route.query.type === 'gb28181-2016'">
:columns="columns" <div class="left-content" :class="{ active: show }">
target="product" <Tree
@search="handleSearch" :deviceId="deviceId"
/> :on-tree-load="(e) => (show = e)"
:on-select="handleSelect"
<JProTable />
ref="listRef" </div>
:columns="columns" <div
:request="(e:any) => ChannelApi.list(e, route?.query.id as string)" class="left-warp--btn"
:defaultParams="{ :class="{ active: !show }"
sorts: [{ name: 'notifyTime', order: 'desc' }], @click="show = !show"
}"
:params="params"
model="table"
>
<template #headerTitle>
<a-tooltip
v-if="route?.query.type === 'gb28181-2016'"
title="接入方式为GB/T28281时不支持新增"
> >
<a-button type="primary" disabled> 新增 </a-button> <AIcon type="LeftOutlined" />
</a-tooltip> </div>
<a-button type="primary" @click="handleAdd" v-else> </div>
新增 <div class="right">
</a-button> <j-advanced-search
</template> type="simple"
<template #status="slotProps"> :columns="columns"
<a-space> target="product"
<a-badge @search="handleSearch"
:status=" />
slotProps.status.value === 'online'
? 'success' <JProTable
: 'error' ref="listRef"
" :columns="columns"
:text="slotProps.status.text" :request="(e:any) => ChannelApi.list(e, route?.query.id as string)"
></a-badge> :defaultParams="{
</a-space> sorts: [{ name: 'notifyTime', order: 'desc' }],
</template> }"
<template #action="slotProps"> :params="params"
<a-space :size="16"> model="table"
<a-tooltip >
v-for="i in getActions(slotProps, 'table')" <template #headerTitle>
:key="i.key" <j-tooltip
v-bind="i.tooltip" v-if="route?.query.type === 'gb28181-2016'"
> title="接入方式为GB/T28281时不支持新增"
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
> >
<a-button <j-button type="primary" disabled> 新增 </j-button>
:disabled="i.disabled" </j-tooltip>
style="padding: 0" <j-button type="primary" @click="handleAdd" v-else>
type="link" 新增
><AIcon :type="i.icon" </j-button>
/></a-button> </template>
</a-popconfirm> <template #status="slotProps">
<a-button <j-space>
style="padding: 0" <j-badge
type="link" :status="
v-else slotProps.status.value === 'online'
@click="i.onClick && i.onClick(slotProps)" ? 'success'
> : 'error'
<a-button "
:disabled="i.disabled" :text="slotProps.status.text"
style="padding: 0" ></j-badge>
type="link" </j-space>
><AIcon :type="i.icon" </template>
/></a-button> <template #action="slotProps">
</a-button> <j-space :size="16">
</a-tooltip> <j-tooltip
</a-space> v-for="i in getActions(slotProps, 'table')"
</template> :key="i.key"
</JProTable> 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 <Save
v-model:visible="saveVis" v-model:visible="saveVis"
@ -94,7 +114,9 @@ import { useMenuStore } from 'store/menu';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import Save from './Save.vue'; import Save from './Save.vue';
import Live from './Live/index.vue'; import Live from './Live/index.vue';
import Tree from './Tree/index.vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { useElementSize } from '@vueuse/core';
const menuStory = useMenuStore(); const menuStory = useMenuStore();
const route = useRoute(); const route = useRoute();
@ -253,4 +275,25 @@ const getActions = (
? actions.filter((f) => f.key !== 'delete') ? actions.filter((f) => f.key !== 'delete')
: actions; : 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> </script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,76 @@
<template> <template>
<page-container> <page-container>
<a-row :gutter="24"> <j-row :gutter="24">
<a-col :span="14"> <j-col :span="14">
<BootCard <BootCard
:cardData="deviceBootConfig" :cardData="deviceBootConfig"
cardTitle="视频中心引导" cardTitle="视频中心引导"
/> />
</a-col> </j-col>
<a-col :span="10"> <j-col :span="10">
<BasicCountCard /> <BasicCountCard />
</a-col> </j-col>
<a-col :span="24" style="margin: 20px 0"> <j-col :span="24" style="margin: 20px 0">
<PlatformPicCard /> <PlatformPicCard />
</a-col> </j-col>
<a-col :span="24"> <j-col :span="24">
<StepCard <StepCard
cardTitle="设备接入推荐步骤" cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异" tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="deviceStepDetails" :dataList="deviceStepDetails"
/> />
</a-col> </j-col>
</a-row> </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> </page-container>
</template> </template>
@ -33,6 +83,13 @@ import BasicCountCard from '@/views/media/Home/components/BasicCountCard.vue';
import { usePermissionStore } from '@/store/permission'; import { usePermissionStore } from '@/store/permission';
import type { bootConfig, recommendList } from '@/views/home/typing'; 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; const hasPermission = usePermissionStore().hasPermission;
@ -40,59 +97,116 @@ const deviceBootConfig: bootConfig[] = [
{ {
english: 'STEP1', english: 'STEP1',
label: '添加视频设备', label: '添加视频设备',
link: '/media/device/Save', link: 'media/Device/Save',
auth: hasPermission('/media/device/Save'), auth: hasPermission('media/Device:add'),
params: {
save: true,
},
}, },
{ {
english: 'STEP2', english: 'STEP2',
label: '分屏展示', label: '分屏展示',
link: '/media/SplitScreen', link: 'media/SplitScreen',
auth: hasPermission('/media/SplitScreen'),
params: {
save: true,
},
}, },
{ {
english: 'STEP3', english: 'STEP3',
label: '国标级联', label: '国标级联',
link: '/media/Cascade', link: 'media/Cascade',
auth: hasPermission('/media/Cascade'),
params: {
save: true,
},
}, },
]; ];
const deviceStepDetails: recommendList[] = [ const deviceStepDetails: recommendList[] = [
{ {
title: '添加视频设备', title: '添加视频设备',
details: '根据视频设备的传输协议,在已创建的产品下添加对应的设备。', details: '根据视频设备的传输协议,在已创建的产品下添加对应的设备。',
iconUrl: '/images/home/bottom-6.png', iconUrl: '/images/home/bottom-6.png',
linkUrl: '/media/device/Save', linkUrl: 'media/Device/Save',
auth: hasPermission('/media/device/Save'), auth: hasPermission('media/Device:add'),
params: {
save: true,
},
}, },
{ {
title: '查看通道', title: '查看通道',
details: '查看设备下的通道数据,可以进行直播、录制等操作。', details: '查看设备下的通道数据,可以进行直播、录制等操作。',
iconUrl: '/images/home/bottom-7.png', iconUrl: '/images/home/bottom-7.png',
linkUrl: '/media/device/Channel', // linkUrl: 'media/Device/Channel',
auth: hasPermission('/media/device/Save'), linkUrl: '',
dialogTag: 'accessMethod', auth: hasPermission('media/Device:view'),
onClick: (row: any) => {
if (hasPermission('media/Device:view')) {
visible.value = true;
} else {
message.warning('暂无权限,请联系管理员');
}
},
}, },
{ {
title: '分屏展示', title: '分屏展示',
details: '对多个通道的视频流数据进行分屏展示。', details: '对多个通道的视频流数据进行分屏展示。',
iconUrl: '/images/home/bottom-8.png', iconUrl: '/images/home/bottom-8.png',
linkUrl: '/media/SplitScreen', linkUrl: 'media/SplitScreen',
auth: hasPermission('/media/SplitScreen'), },
params: { ];
save: true,
//
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> </script>

View File

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

View File

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

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