fix: 视频中心仪表盘兼容数据处理;

* fix: bug#28509【设备】设备物模型属性类型为地理位置时,运行状态查看详情,选择图标展示筛选条件错误

* fix: bug#28513 【设备】设备详情界面,物联网卡和远程升级需做权限控制,当没有权限时,隐藏当前tab

* fix: 视频中心仪表盘兼容数据处理

* chore: add dayjs

* update: docker tag
This commit is contained in:
XieYongHong 2024-08-07 15:52:46 +08:00 committed by GitHub
parent 984fd9c115
commit 3da54bb59b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 171 additions and 110 deletions

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-TEST . docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-TEST docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT

View File

@ -1,7 +1,7 @@
{ {
"name": "jetlinks-vue", "name": "jetlinks-vue",
"private": true, "private": true,
"version": "2.2.0", "version": "2.2.1",
"scripts": { "scripts": {
"dev": "vite --mode develop", "dev": "vite --mode develop",
"dev:force": "vite --force --mode develop", "dev:force": "vite --force --mode develop",
@ -29,6 +29,7 @@
"axios": "^1.2.1", "axios": "^1.2.1",
"colorpicker-v3": "^2.10.2", "colorpicker-v3": "^2.10.2",
"cronstrue": "^2.50.0", "cronstrue": "^2.50.0",
"dayjs": "^1.11.12",
"driver.js": "^0.9.8", "driver.js": "^0.9.8",
"echarts": "^5.4.1", "echarts": "^5.4.1",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",

View File

@ -2,7 +2,7 @@
<j-pro-layout <j-pro-layout
v-bind="layoutConf" v-bind="layoutConf"
v-model:collapsed="basicLayout.collapsed" v-model:collapsed="basicLayout.collapsed"
v-model:openKeys="basicLayout.openKeys" :openKeys="basicLayout.collapsed ? [] : basicLayout.openKeys"
:selectedKeys="basicLayout.selectedKeys" :selectedKeys="basicLayout.selectedKeys"
:breadcrumb="basicLayout.pure ? undefined : { routes: breadcrumbs }" :breadcrumb="basicLayout.pure ? undefined : { routes: breadcrumbs }"
:headerHeight='basicLayout.pure ? 1 : layout.headerHeight' :headerHeight='basicLayout.pure ? 1 : layout.headerHeight'

View File

@ -257,7 +257,10 @@ defineExpose({
//position: absolute; //position: absolute;
transition: top .2s, height .2s, background-color .1s; transition: top .2s, height .2s, background-color .1s;
border-bottom: 1px solid #ebebeb; border-bottom: 1px solid #ebebeb;
.metadata-edit-table-cell{
position:absolute;
min-width: 0;;
}
&:hover { &:hover {
background-color: rgb(248, 248, 248); background-color: rgb(248, 248, 248);
} }

View File

@ -233,7 +233,7 @@ watch(
(val) => { (val) => {
const diffInSeconds = dayjs(val[1]).diff(dayjs(val[0]), 'minute'); const diffInSeconds = dayjs(val[1]).diff(dayjs(val[0]), 'minute');
if (diffInSeconds < 60) { if (diffInSeconds < 60) {
periodOptions.value = [ periodOptions.value = _type.value ? [
{ {
label: '实际值', label: '实际值',
value: '*', value: '*',
@ -242,10 +242,15 @@ watch(
label: '按分钟统计', label: '按分钟统计',
value: '1m', value: '1m',
}, },
]; ] : [
cycle.value = '*'; {
label: '按分钟统计',
value: '1m',
},
]
cycle.value = _type.value ? '*' : '1m';
} else if (diffInSeconds < 1440) { } else if (diffInSeconds < 1440) {
periodOptions.value = [ periodOptions.value = _type.value ? [
{ {
label: '实际值', label: '实际值',
value: '*', value: '*',
@ -258,8 +263,17 @@ watch(
label: '按小时统计', label: '按小时统计',
value: '1h', value: '1h',
}, },
]; ] : [
cycle.value = '*'; {
label: '按分钟统计',
value: '1m',
},
{
label: '按小时统计',
value: '1h',
},
]
cycle.value = _type.value ? '*' : '1m';
} else if (diffInSeconds < 43200) { } else if (diffInSeconds < 43200) {
periodOptions.value = [ periodOptions.value = [
{ {

View File

@ -170,14 +170,6 @@ const initList = [
key: 'Log', key: 'Log',
tab: '日志管理', tab: '日志管理',
}, },
{
key: 'CardManagement',
tab: '物联网卡',
},
{
key: 'Firmware',
tab: '远程升级',
},
]; ];
const list = ref([...initList]); const list = ref([...initList]);
@ -226,6 +218,18 @@ const getDetail = () => {
tab: '预处理数据', tab: '预处理数据',
}); });
} }
if (permissionStore.hasPermission('iot-card/CardManagement:view')) {
list.value.push({
key: 'CardManagement',
tab: '物联网卡',
});
}
if (permissionStore.hasPermission('device/Firmware:view')) {
list.value.push({
key: 'Firmware',
tab: '远程升级',
});
}
if ( if (
instanceStore.current?.protocol && instanceStore.current?.protocol &&
!['modbus-tcp', 'opc-ua'].includes(instanceStore.current?.protocol) && !['modbus-tcp', 'opc-ua'].includes(instanceStore.current?.protocol) &&
@ -333,13 +337,8 @@ const getDetailFn = async () => {
} else { } else {
instanceStore.tabActiveKey = routerParams.params.value.tab || 'Info'; instanceStore.tabActiveKey = routerParams.params.value.tab || 'Info';
} }
}; };
onMounted(() => {
getDetailFn();
});
const onTabChange = (e: string) => { const onTabChange = (e: string) => {
if (instanceStore.tabActiveKey === 'Metadata') { if (instanceStore.tabActiveKey === 'Metadata') {
EventEmitter.emit('MetadataTabs', () => { EventEmitter.emit('MetadataTabs', () => {
@ -389,6 +388,10 @@ const jumpProduct = () => {
}); });
}; };
onMounted(() => {
getDetailFn();
});
onUnmounted(() => { onUnmounted(() => {
instanceStore.current = {} as any; instanceStore.current = {} as any;
statusRef.value && statusRef.value.unsubscribe(); statusRef.value && statusRef.value.unsubscribe();

View File

@ -193,10 +193,6 @@ const list = ref([
key: 'Device', key: 'Device',
tab: '设备接入', tab: '设备接入',
}, },
{
key: 'Firmware',
tab: '远程升级',
},
]); ]);
const tabs = { const tabs = {
@ -300,10 +296,6 @@ const getProtocol = async () => {
key: 'DataAnalysis', key: 'DataAnalysis',
tab: '数据解析', tab: '数据解析',
}, },
{
key: 'Firmware',
tab: '远程升级',
},
]; ];
} else { } else {
list.value = [ list.value = [
@ -320,10 +312,6 @@ const getProtocol = async () => {
key: 'Device', key: 'Device',
tab: '设备接入', tab: '设备接入',
}, },
{
key: 'Firmware',
tab: '远程升级',
},
]; ];
} }
} }
@ -348,6 +336,12 @@ const getProtocol = async () => {
tab: '预处理数据', tab: '预处理数据',
}); });
} }
if (permissionStore.hasPermission('device/Firmware:view')) {
list.value.push({
key: 'Firmware',
tab: '远程升级',
});
}
} }
}; };
/** /**

View File

@ -1,19 +1,19 @@
<!-- 国标级联-绑定通道 --> <!-- 国标级联-绑定通道 -->
<template> <template>
<j-modal <j-modal
v-model:visible="_vis" visible
title="绑定通道" title="绑定通道"
cancelText="取消" cancelText="取消"
okText="确定" okText="确定"
width="80%" width="80%"
@ok="handleSave" @ok="handleSave"
@cancel="_vis = false" @cancel="$emit('cancel')"
:confirmLoading="loading" :confirmLoading="loading"
> >
<pro-search <pro-search
:columns="columns" :columns="columns"
target="media-bind"
@search="handleSearch" @search="handleSearch"
type="simple"
/> />
<JProTable <JProTable
@ -68,37 +68,15 @@
<script setup lang="ts"> <script setup lang="ts">
import CascadeApi from '@/api/media/cascade'; import CascadeApi from '@/api/media/cascade';
import { onlyMessage } from '@/utils/comm'; import { onlyMessage } from '@/utils/comm';
import { PropType } from 'vue';
const route = useRoute(); const route = useRoute();
type Emits = { type Emits = {
(e: 'update:visible', data: boolean): void; (e: 'cancel'): void;
(e: 'submit'): void; (e: 'submit'): void;
}; };
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const props = defineProps({
visible: { type: Boolean, default: false },
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
const _vis = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
});
watch(
() => _vis.value,
(val) => {
if (val) handleSearch({ terms: [] });
else _selectedRowKeys.value = [];
},
);
const columns = [ const columns = [
{ {
title: '设备名称', title: '设备名称',
@ -211,10 +189,13 @@ const handleSave = async () => {
}) })
if (resp.success) { if (resp.success) {
onlyMessage('操作成功!'); onlyMessage('操作成功!');
_vis.value = false;
emit('submit'); emit('submit');
} else { } else {
onlyMessage('操作失败!', 'error'); onlyMessage('操作失败!', 'error');
} }
}; };
onMounted(()=>{
handleSearch({ terms: [] })
})
</script> </script>

View File

@ -138,7 +138,7 @@
</JProTable> </JProTable>
</FullPage> </FullPage>
<BindChannel v-model:visible="bindVis" @submit="listRef.reload()" /> <BindChannel v-if="bindVis" @submit="submitData" @cancel="cancel"/>
</page-container> </page-container>
</template> </template>
@ -420,6 +420,14 @@ const handleClose = (data: any) => {
valid.value = undefined; valid.value = undefined;
gbID.value = ''; gbID.value = '';
}; };
const cancel = () =>{
bindVis.value = false
}
const submitData = () =>{
bindVis.value = false
listRef.value?.reload()
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.header { .header {

View File

@ -617,7 +617,7 @@ const getClustersList = async () => {
value: m.id, value: m.id,
})); }));
}; };
getClustersList();
/** /**
* SIP本地地址 * SIP本地地址
*/ */
@ -631,7 +631,7 @@ const getAllList = async () => {
})); }));
setPorts(); setPorts();
}; };
getAllList();
const handleTransportChange = () => { const handleTransportChange = () => {
formData.value.host = undefined; formData.value.host = undefined;
@ -671,9 +671,6 @@ const getDetail = async () => {
// console.log('formData.value: ', formData.value); // console.log('formData.value: ', formData.value);
}; };
onMounted(() => {
getDetail();
});
const regDomain = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.|$)){4}$/ const regDomain = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.|$)){4}$/
// /[j-zA-Z0-9][-j-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}(\.[j-zA-Z0-9][-j-zA-Z0-9]{0,62})+\.?/;
@ -778,6 +775,12 @@ const handleSubmit = () => {
console.log('err: ', err); console.log('err: ', err);
}); });
}; };
onMounted(async() => {
await getDetail();
getClustersList();
getAllList();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -21,9 +21,9 @@
</div> </div>
<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>
<j-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>
</div> </div>

View File

@ -16,7 +16,7 @@
.media-live-video { .media-live-video {
position: relative; position: relative;
flex: 1; flex: 1;
min-height: 490px;
.media-tool { .media-tool {
position: absolute; position: absolute;
top: 4px; top: 4px;

View File

@ -5,7 +5,7 @@
<div class="actions-item" v-if="item.executor === 'alarm'"> <div class="actions-item" v-if="item.executor === 'alarm'">
<div class="item-options-warp"> <div class="item-options-warp">
<div class="item-options-type"> <div class="item-options-type">
<img <!-- <img
style="width: 18px" style="width: 18px"
:src=" :src="
iconMap.get( iconMap.get(
@ -14,7 +14,8 @@
: item.executor, : item.executor,
) )
" "
/> /> -->
<AIcon :type="iconMap.get(item.executor === 'alarm' ? item.alarm.mode : item.executor)"/>
</div> </div>
<div class="item-options-content"> <div class="item-options-content">
<template v-if="item.executor === 'alarm'"> <template v-if="item.executor === 'alarm'">
@ -75,6 +76,7 @@ const props = defineProps({
background-color: #f0f0f0; background-color: #f0f0f0;
border-radius: 6px 0 0 6px; border-radius: 6px 0 0 6px;
cursor: pointer; cursor: pointer;
font-size: 22px;
} }
.item-options-content { .item-options-content {

View File

@ -257,11 +257,12 @@ const columns = [
}, },
{ {
title: '关联场景联动', title: '关联场景联动',
dataIndex: 'scene', dataIndex: 'id',
hideInTable:true, hideInTable:true,
key: 'scene', key: 'id',
search: { search: {
type: 'select', type: 'select',
termOptions: ['in'],
options: async () => { options: async () => {
const allData = await queryList({ const allData = await queryList({
paging: false, paging: false,
@ -355,6 +356,14 @@ const map = {
other: '其他', other: '其他',
}; };
const handleSearch = (e: any) => { const handleSearch = (e: any) => {
e.terms.map((i:any)=>{
i.terms.forEach((item:any)=>{
if(item.column === 'id'){
item.termType = 'rule-bind-alarm'
}
})
})
console.log(e,'e')
params.value = e; params.value = e;
}; };

View File

@ -287,7 +287,7 @@
.clientId .clientId
" "
placeholder="请输入appId" placeholder="请输入appId"
:disabled="!!form.data.id" :disabled="!!form.data.id && !!form.data.apiClient.authConfig.oauth2.clientId"
/> />
</j-form-item> </j-form-item>
<j-form-item <j-form-item

View File

@ -241,11 +241,13 @@
</j-form-item> </j-form-item>
<j-form-item label="权限"> <j-form-item label="权限">
<PermissChoose <PermissChoose
v-if="showPermissionChoose"
:first-width="3" :first-width="3"
max-height="350px" max-height="350px"
v-model:value="form.data.permissions" v-model:value="form.data.permissions"
:key="form.data.id || ''" :key="form.data.id || ''"
/> />
<a-spin v-else/>
</j-form-item> </j-form-item>
</j-form> </j-form>
</div> </div>
@ -291,6 +293,7 @@ const permission = 'system/Menu';
// //
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const showPermissionChoose = ref(false)
const routeParams = { const routeParams = {
id: route.params.id === ':id' ? undefined : (route.params.id as string), id: route.params.id === ':id' ? undefined : (route.params.id as string),
...route.query, ...route.query,
@ -334,6 +337,7 @@ const form = reactive({
resp.result?.accessSupport?.value || 'unsupported', resp.result?.accessSupport?.value || 'unsupported',
}; };
form.sourceCode = resp.result.code; form.sourceCode = resp.result.code;
showPermissionChoose.value = true
}); });
if (isNoCommunity) { if (isNoCommunity) {

View File

@ -26,7 +26,7 @@
<h5>请求示例</h5> <h5>请求示例</h5>
<JsonViewer :value="requestCard.codeText" copyable /> <JsonViewer :value="requestCard.codeText" copyable />
</div> </div>
<div class="api-card"> <div class="api-card" v-if="requestCard.tableData.length">
<h5>请求参数</h5> <h5>请求参数</h5>
<div class="content"> <div class="content">
<j-pro-table <j-pro-table
@ -91,7 +91,7 @@ import 'vue3-json-viewer/dist/index.css';
import type { apiDetailsType } from '../typing'; import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue'; import InputCard from './InputCard.vue';
import { PropType } from 'vue'; import { PropType } from 'vue';
import { findData, getCodeText } from '../utils'; import { findData, getCodeText, dealNoRef } from '../utils';
type cardType = { type cardType = {
columns: object[]; columns: object[];
@ -101,7 +101,6 @@ type cardType = {
getData: Function; getData: Function;
}; };
const props = defineProps({ const props = defineProps({
selectApi: { selectApi: {
type: Object as PropType<apiDetailsType>, type: Object as PropType<apiDetailsType>,
@ -114,7 +113,6 @@ const props = defineProps({
}); });
const { selectApi } = toRefs(props); const { selectApi } = toRefs(props);
const requestCard = reactive<cardType>({ const requestCard = reactive<cardType>({
columns: [ columns: [
{ {
@ -153,12 +151,14 @@ const requestCard = reactive<cardType>({
const schema = const schema =
props.selectApi.requestBody.content['application/json'].schema; props.selectApi.requestBody.content['application/json'].schema;
const _ref = schema.$ref || schema?.items?.$ref; const _ref = schema.$ref || schema?.items?.$ref;
if (!_ref) return; // schemaJava // schemaJava
if (!_ref) {
const type = schema.type || '';
requestCard.codeText = dealNoRef(type, schema);
} else {
const schemaName = _ref?.split('/').pop(); const schemaName = _ref?.split('/').pop();
const type = schema.type || ''; const type = schema.type || '';
const tableData = findData(props.schemas, schemaName); const tableData = findData(props.schemas, schemaName);
requestCard.codeText = requestCard.codeText =
type === 'array' type === 'array'
? [getCodeText(props.schemas, tableData, 3)] ? [getCodeText(props.schemas, tableData, 3)]
@ -178,6 +178,7 @@ const requestCard = reactive<cardType>({
})), })),
}, },
]; ];
}
}, },
}); });
const responseStatusCard = reactive<cardType>({ const responseStatusCard = reactive<cardType>({

View File

@ -108,7 +108,7 @@
</j-button> </j-button>
</div> </div>
<j-monaco-editor <j-monaco-editor
v-if=" method !=='get' && method !=='patch'" v-if="showRequestBody"
ref="editorRef" ref="editorRef"
language="json" language="json"
style="height: 100% ; min-height: 200px;" style="height: 100% ; min-height: 200px;"
@ -144,6 +144,7 @@ const responsesContent = ref({});
const editorRef = ref(); const editorRef = ref();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const method = ref() const method = ref()
const showRequestBody = ref(!!props.selectApi?.requestBody)
const requestBody = reactive({ const requestBody = reactive({
tableColumns: [ tableColumns: [
{ {

View File

@ -7,7 +7,7 @@ import { schemaObjType } from "./typing";
* @param schemaName * @param schemaName
*/ */
export function findData(schemas: object, schemaName: string) { export function findData(schemas: object, schemaName: string) {
const basicType = ['string', 'integer', 'boolean']; const basicType = ['string', 'integer', 'boolean','number'];
if (!schemaName || !schemas[schemaName]) { if (!schemaName || !schemas[schemaName]) {
return []; return [];
@ -62,6 +62,9 @@ export function getCodeText(
case 'object': case 'object':
result[item.paramsName] = ''; result[item.paramsName] = '';
break; break;
case 'number':
result[item.paramsName] = 0;
break;
default: { default: {
const properties = schemas[item.paramsType]?.properties as object || {}; const properties = schemas[item.paramsType]?.properties as object || {};
const newArr = Object.entries(properties).map( const newArr = Object.entries(properties).map(
@ -87,3 +90,32 @@ export function getCodeText(
return result; return result;
} }
/**
* $ref的情况
*/
export function dealNoRef(type:string,schema?:any):any{
let result = undefined;
switch (type) {
case 'string':
result = '';
break;
case 'integer':
result = 0;
break;
case 'boolean':
result = true;
break;
case 'array':
const itemType = schema?.items?.type
const item = dealNoRef(itemType)
result = [item];
break;
case 'number':
result = 0;
break;
default:
return;
}
return result
}

View File

@ -2293,6 +2293,11 @@ dayjs@^1.10.5:
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
dayjs@^1.11.12:
version "1.11.12"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d"
integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==
de-indent@^1.0.2: de-indent@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"