Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
leiqiaochu 2023-03-07 10:14:26 +08:00
commit 41e1ca9f9c
83 changed files with 3209 additions and 1694 deletions

49
src/api/media/playback.ts Normal file
View File

@ -0,0 +1,49 @@
import server from '@/utils/request';
import { LocalStore } from '@/utils/comm';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { recordsItemType } from '@/views/media/Device/Playback/typings';
export default {
// 开始直播
ptzStart: (deviceId: string, channelId: string, type: string) =>
`${BASE_API_PATH}/media/device/${deviceId}/${channelId}/live.${type}?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`,
// 查询设备通道详情
queryDetail: (deviceId: string, data: any) => server.post(`/media/device/${deviceId}/channel/_query`, data),
// 查询本地回放记录
queryRecordLocal: (deviceId: string, channelId: string, data?: any) =>
server.post<any>(`/media/device/${deviceId}/${channelId}/records/in-local`, data),
// 下载到云端
downloadRecord: (deviceId: string, channelId: string, data: any) =>
server.post(`/media/device/${deviceId}/${channelId}/_record`, data),
// 播放本地回放
playbackLocal: (
deviceId: string,
channelId: string,
suffix: string,
startTime: string,
endTime: string,
speed: number = 1
) =>
`${BASE_API_PATH}/media/device/${deviceId}/${channelId}/playback.${suffix}?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&startTime=${startTime}&endTime=${endTime}&speed=${speed}`,
// 本地录像播放控制
playbackControl: (deviceId: string, channelId: string) =>
server.post(`/media/device/${deviceId}/${channelId}/stream-control`),
// 查询云端回放记录
recordsInServer: (deviceId: string, channelId: string, data: any) =>
server.post<recordsItemType[]>(`/media/device/${deviceId}/${channelId}/records/in-server`, data),
// 查询云端回放文件信息
recordsInServerFiles: (deviceId: string, channelId: string, data: any) =>
server.post<recordsItemType[]>(`/media/device/${deviceId}/${channelId}/records/in-server/files`, data),
// 播放云端回放
playbackStart: (recordId: string) => `${BASE_API_PATH}/record/${recordId}.mp4?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`,
downLoadFile: (recordId: string) => `${BASE_API_PATH}/record/${recordId}.mp4?download=true&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
}

View File

@ -73,7 +73,9 @@ const iconKeys = [
'BellOutlined', 'BellOutlined',
'UserOutlined', 'UserOutlined',
'LogoutOutlined', 'LogoutOutlined',
'ReadIconOutlined' 'ReadIconOutlined',
'CloudDownloadOutlined',
'PauseCircleOutlined',
] ]
const Icon = (props: {type: string}) => { const Icon = (props: {type: string}) => {

View File

@ -28,5 +28,4 @@ const props = defineProps({
*/ */
statusNames: { type: Object }, statusNames: { type: Object },
}); });
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="upload-image-warp"> <div class="upload-image-warp">
<div class="upload-image-border"> <div class="upload-image-border">
<a-upload <j-upload
name="file" name="file"
list-type="picture-card" list-type="picture-card"
class="avatar-uploader" class="avatar-uploader"
@ -45,7 +45,7 @@
/> />
</template> </template>
</div> </div>
</a-upload> </j-upload>
<div class="upload-loading-mask" v-if="props.disabled"></div> <div class="upload-loading-mask" v-if="props.disabled"></div>
<div class="upload-loading-mask" v-if="imageUrl && loading"> <div class="upload-loading-mask" v-if="imageUrl && loading">
<AIcon type="LoadingOutlined" style="font-size: 20px" /> <AIcon type="LoadingOutlined" style="font-size: 20px" />

View File

@ -5,7 +5,7 @@
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="player-screen-tool" v-if="showScreen"> <div class="player-screen-tool" v-if="showScreen">
<a-radio-group <a-radio-group
v-model:value="screen" :value="screen"
button-style="solid" button-style="solid"
@change="handleScreenChange" @change="handleScreenChange"
> >
@ -208,9 +208,7 @@ const formData = ref({
// //
const fullscreenRef = ref(null); const fullscreenRef = ref(null);
const { isFullscreen, enter, exit, toggle } = useFullscreen( const { isFullscreen, enter, exit, toggle } = useFullscreen(fullscreenRef);
fullscreenRef.value,
);
/** /**
* 刷新视频 * 刷新视频

View File

@ -30,7 +30,7 @@ const options = reactive({
muted: false, // muted: false, //
webFullScreen: false, webFullScreen: false,
speedRate: ['0.75', '1.0', '1.25', '1.5', '2.0'], // speedRate: ['0.75', '1.0', '1.25', '1.5', '2.0'], //
autoPlay: true, // autoPlay: false, //
loop: false, // loop: false, //
mirror: false, // mirror: false, //
ligthOff: false, // ligthOff: false, //

View File

@ -26,7 +26,7 @@
<div <div
:class="[ :class="[
'checked-icon', 'checked-icon',
disabled && myValue === item.value (disabled && myValue === item.value) || item.disabled
? 'checked-icon-disabled' ? 'checked-icon-disabled'
: '', : '',
]" ]"
@ -45,6 +45,7 @@ interface IOption {
label: string; label: string;
value: string; value: string;
logo: string; logo: string;
disabled?: boolean;
} }
type Emits = { type Emits = {

View File

@ -1,14 +1,14 @@
<!-- 参数类型输入组件 --> <!-- 参数类型输入组件 -->
<template> <template>
<div class="wrapper"> <div class="wrapper">
<a-select <j-select
v-if="typeMap.get(itemType) === 'select'" v-if="typeMap.get(itemType) === 'select'"
v-model:value="myValue" v-model:value="myValue"
:options="options" :options="options"
allowClear allowClear
style="width: 100%" style="width: 100%"
/> />
<a-date-picker <j-date-picker
v-else-if="typeMap.get(itemType) === 'date'" v-else-if="typeMap.get(itemType) === 'date'"
v-model:value="myValue" v-model:value="myValue"
allowClear allowClear
@ -17,13 +17,13 @@
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
style="width: 100%" style="width: 100%"
/> />
<a-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%"
/> />
<a-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"
@ -31,19 +31,19 @@
<template #addonAfter> <template #addonAfter>
<form-outlined @click="modalVis = true" /> <form-outlined @click="modalVis = true" />
</template> </template>
</a-input> </j-input>
<GeoComponent <GeoComponent
v-else-if="typeMap.get(itemType) === 'geoPoint'" v-else-if="typeMap.get(itemType) === 'geoPoint'"
v-model:point="myValue" v-model:point="myValue"
/> />
<a-input <j-input
v-else-if="typeMap.get(itemType) === 'file'" v-else-if="typeMap.get(itemType) === 'file'"
v-model:value="myValue" v-model:value="myValue"
placeholder="请输入图片链接" placeholder="请输入图片链接"
allowClear allowClear
> >
<template #addonAfter> <template #addonAfter>
<a-upload <j-upload
name="file" name="file"
:action="FILE_UPLOAD" :action="FILE_UPLOAD"
:headers="headers" :headers="headers"
@ -51,10 +51,10 @@
@change="handleFileChange" @change="handleFileChange"
> >
<cloud-upload-outlined /> <cloud-upload-outlined />
</a-upload> </j-upload>
</template> </template>
</a-input> </j-input>
<a-input <j-input
v-else v-else
allowClear allowClear
type="text" type="text"
@ -63,7 +63,7 @@
/> />
<!-- 代码编辑器弹窗 --> <!-- 代码编辑器弹窗 -->
<a-modal <j-modal
title="编辑" title="编辑"
ok-text="确认" ok-text="确认"
cancel-text="取消" cancel-text="取消"
@ -75,13 +75,12 @@
<div style="width: 100%; height: 400px"> <div style="width: 100%; height: 400px">
<MonacoEditor v-model:modelValue="objectValue" /> <MonacoEditor v-model:modelValue="objectValue" />
</div> </div>
</a-modal> </j-modal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PropType } from 'vue'; import { PropType } from 'vue';
import { FormOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { UploadChangeParam, UploadFile } from 'ant-design-vue'; import { UploadChangeParam, UploadFile } from 'ant-design-vue';
import { DefaultOptionType } from 'ant-design-vue/lib/select'; import { DefaultOptionType } from 'ant-design-vue/lib/select';
import MonacoEditor from '@/components/MonacoEditor/index.vue'; import MonacoEditor from '@/components/MonacoEditor/index.vue';

View File

@ -8,7 +8,7 @@ import CardBox from './CardBox/index.vue';
// import Search from './Search' // import Search from './Search'
import NormalUpload from './NormalUpload/index.vue' import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue' import FileFormat from './FileFormat/index.vue'
// import JUpload 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/es/components' import { PageContainer } from 'jetlinks-ui-components/es/components'
import Ellipsis from './Ellipsis/index.vue' import Ellipsis from './Ellipsis/index.vue'
@ -27,7 +27,7 @@ export default {
// .component('Search', Search) // .component('Search', Search)
.component('NormalUpload', NormalUpload) .component('NormalUpload', NormalUpload)
.component('FileFormat', FileFormat) .component('FileFormat', FileFormat)
// .component('JUpload', JUpload) .component('JProUpload', JProUpload)
.component('BasicLayoutPage', BasicLayoutPage) .component('BasicLayoutPage', BasicLayoutPage)
.component('BlankLayoutPage', BlankLayoutPage) .component('BlankLayoutPage', BlankLayoutPage)
.component('PageContainer', PageContainer) .component('PageContainer', PageContainer)

View File

@ -1,5 +1,5 @@
<template> <template>
<a-table <j-table
rowKey="id" rowKey="id"
:columns="columns" :columns="columns"
:data-source="dataSource" :data-source="dataSource"
@ -36,7 +36,7 @@
</template> </template>
</div> </div>
</template> </template>
</a-table> </j-table>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,60 +1,60 @@
<template> <template>
<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" v-if="actionType === 'command'"> <j-col :span="24" v-if="actionType === 'command'">
<a-form-item name="messageType" label="指令类型" :rules="{ <j-form-item name="messageType" label="指令类型" :rules="{
required: true, required: true,
message: '请选择指令类型', message: '请选择指令类型',
}"> }">
<a-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption"> <j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption">
<a-select-option value="READ_PROPERTY">读取属性</a-select-option> <j-select-option value="READ_PROPERTY">读取属性</j-select-option>
<a-select-option value="WRITE_PROPERTY">修改属性</a-select-option> <j-select-option value="WRITE_PROPERTY">修改属性</j-select-option>
<a-select-option value="INVOKE_FUNCTION">调用功能</a-select-option> <j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option>
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="(modelRef.messageType === 'READ_PROPERTY' || actionType === 'latestData') ? 24 : 12" v-if="(actionType === 'command' && ['READ_PROPERTY','WRITE_PROPERTY'].includes(modelRef.messageType)) || actionType === 'latestData'"> <j-col :span="(modelRef.messageType === 'READ_PROPERTY' || actionType === 'latestData') ? 24 : 12" v-if="(actionType === 'command' && ['READ_PROPERTY','WRITE_PROPERTY'].includes(modelRef.messageType)) || actionType === 'latestData'">
<a-form-item :name="['message', 'properties']" label="属性" :rules="{ <j-form-item :name="['message', 'properties']" label="属性" :rules="{
required: true, required: true,
message: '请选择属性', message: '请选择属性',
}"> }">
<a-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search :filter-option="filterOption"> <j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search :filter-option="filterOption">
<a-select-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-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>
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="12" v-if="modelRef.messageType === 'WRITE_PROPERTY' && actionType === 'command'"> <j-col :span="12" v-if="modelRef.messageType === 'WRITE_PROPERTY' && actionType === 'command'">
<a-form-item :name="['message', 'value']" label="值" :rules="{ <j-form-item :name="['message', 'value']" label="值" :rules="{
required: true, required: true,
message: '请输入值', message: '请输入值',
}"> }">
<a-input /> <j-input />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'"> <j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
<a-form-item :name="['message', 'functionId']" label="功能" :rules="{ <j-form-item :name="['message', 'functionId']" label="功能" :rules="{
required: true, required: true,
message: '请选择功能', message: '请选择功能',
}"> }">
<a-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 :filter-option="filterOption" @change="funcChange">
<a-select-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-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>
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION' && modelRef.message.functionId"> <j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION' && modelRef.message.functionId">
<a-form-item :name="['message', 'inputs']" label="参数列表" :rules="{ <j-form-item :name="['message', 'inputs']" label="参数列表" :rules="{
required: true, required: true,
message: '请输入参数列表', message: '请输入参数列表',
}"> }">
<EditTable v-model="modelRef.message.inputs"/> <EditTable v-model="modelRef.message.inputs"/>
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
</a-form> </j-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -62,8 +62,6 @@ import EditTable from './EditTable.vue'
const formRef = ref(); const formRef = ref();
const funcList = ref<Record<string, any>[]>([])
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
}; };

View File

@ -18,7 +18,7 @@
<div> <div>
<h2>1在百度小度技能平台创建技能并授权完成物联网平台与DuerOS的关联</h2> <h2>1在百度小度技能平台创建技能并授权完成物联网平台与DuerOS的关联</h2>
<div class="image"> <div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc.jpg')" /> <j-image width="100%" :src="getImage('/cloud/dueros-doc.jpg')" />
</div> </div>
<h1>授权地址</h1> <h1>授权地址</h1>
<div>物联网平台的登录地址注意需要为https</div> <div>物联网平台的登录地址注意需要为https</div>
@ -26,19 +26,19 @@
<h1>Client_Id</h1> <h1>Client_Id</h1>
<div>请填写系统管理-应用管理中的clientId</div> <div>请填写系统管理-应用管理中的clientId</div>
<div class="image"> <div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc1.png')" /> <j-image width="100%" :src="getImage('/cloud/dueros-doc1.png')" />
</div> </div>
<h1>回调地址</h1> <h1>回调地址</h1>
<div>请复制DuerOS平台中的值填写到系统管理-应用管理中-redirectUrl中</div> <div>请复制DuerOS平台中的值填写到系统管理-应用管理中-redirectUrl中</div>
<div class="image"> <div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc2.png')" /> <j-image width="100%" :src="getImage('/cloud/dueros-doc2.png')" />
</div> </div>
<h1>Token地址</h1> <h1>Token地址</h1>
<div>请复制并填写HTTPS://{location.host}/api/v1/token</div> <div>请复制并填写HTTPS://{location.host}/api/v1/token</div>
<h1>ClientSecret</h1> <h1>ClientSecret</h1>
<div>请复制系统管理-应用管理中的secureKey填写到DuerOS平台</div> <div>请复制系统管理-应用管理中的secureKey填写到DuerOS平台</div>
<div class="image"> <div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc3.png')" /> <j-image width="100%" :src="getImage('/cloud/dueros-doc3.png')" />
</div> </div>
<div></div> <div></div>
<h1>WebService</h1> <h1>WebService</h1>

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="12"> <j-col :span="12">
<a-form-item <j-form-item
label="产品" label="产品"
name="id" name="id"
:rules="[ :rules="[
@ -42,7 +42,7 @@
}, },
]" ]"
> >
<a-select <j-select
:disabled=" :disabled="
type !== 'edit' && type !== 'edit' &&
modelRef.id && modelRef.id &&
@ -54,18 +54,18 @@
:filter-option="filterOption" :filter-option="filterOption"
@change="productChange" @change="productChange"
> >
<a-select-option <j-select-option
v-for="item in productList" v-for="item in productList"
: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="12"> <j-col :span="12">
<a-form-item <j-form-item
name="applianceType" name="applianceType"
:rules="{ :rules="{
required: true, required: true,
@ -75,50 +75,50 @@
<template #label> <template #label>
<span> <span>
设备类型 设备类型
<a-tooltip <j-tooltip
title="DuerOS平台拟定的规范" title="DuerOS平台拟定的规范"
> >
<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="modelRef.applianceType" v-model:value="modelRef.applianceType"
show-search show-search
:filter-option="filterOption" :filter-option="filterOption"
@change="typeChange" @change="typeChange"
> >
<a-select-option <j-select-option
v-for="item in typeList" v-for="item in typeList"
: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-form-item <j-form-item
name="productName" name="productName"
v-show="false" v-show="false"
label="产品名称" label="产品名称"
> >
<a-input <j-input
v-model:value="modelRef.productName" v-model:value="modelRef.productName"
/> />
</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.actionMappings.length" v-if="modelRef.actionMappings.length"
:activeKey="actionActiveKey" :activeKey="actionActiveKey"
@change="onActionCollChange" @change="onActionCollChange"
> >
<a-collapse-panel <j-collapse-panel
v-for="( v-for="(
item, index item, index
) in modelRef.actionMappings" ) in modelRef.actionMappings"
@ -139,9 +139,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
:name="[ :name="[
'actionMappings', 'actionMappings',
index, index,
@ -155,16 +155,16 @@
<template #label> <template #label>
<span> <span>
动作 动作
<a-tooltip <j-tooltip
title="DuerOS平台拟定的设备类型具有的相关动作" title="DuerOS平台拟定的设备类型具有的相关动作"
> >
<AIcon <AIcon
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
/> />
</a-tooltip> </j-tooltip>
</span> </span>
</template> </template>
<a-select <j-select
placeholder="请选择动作" placeholder="请选择动作"
v-model:value=" v-model:value="
item.action item.action
@ -174,7 +174,7 @@
filterOption filterOption
" "
> >
<a-select-option <j-select-option
v-for="i in getTypesActions( v-for="i in getTypesActions(
item.action, item.action,
)" )"
@ -183,13 +183,13 @@
: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-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
:name="[ :name="[
'actionMappings', 'actionMappings',
index, index,
@ -203,16 +203,16 @@
<template #label> <template #label>
<span> <span>
操作 操作
<a-tooltip <j-tooltip
title="映射物联网平台中所选产品具备的动作" title="映射物联网平台中所选产品具备的动作"
> >
<AIcon <AIcon
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
/> />
</a-tooltip> </j-tooltip>
</span> </span>
</template> </template>
<a-select <j-select
placeholder="请选择操作" placeholder="请选择操作"
v-model:value=" v-model:value="
item.actionType item.actionType
@ -222,22 +222,22 @@
filterOption filterOption
" "
> >
<a-select-option <j-select-option
value="command" value="command"
>下发指令</a-select-option >下发指令</j-select-option
> >
<a-select-option <j-select-option
value="latestData" value="latestData"
>获取历史数据</a-select-option >获取历史数据</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col <j-col
:span="24" :span="24"
v-if="item.actionType" v-if="item.actionType"
> >
<a-form-item <j-form-item
:name="[ :name="[
'actionMappings', 'actionMappings',
index, index,
@ -256,14 +256,14 @@
item.actionType item.actionType
" "
/> />
</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-col>
<a-col :span="24"> <j-col :span="24">
<a-button <j-button
type="dashed" type="dashed"
style="width: 100%; margin-top: 10px" style="width: 100%; margin-top: 10px"
@click="addItem" @click="addItem"
@ -272,16 +272,16 @@
type="PlusOutlined" type="PlusOutlined"
style="margin-left: 2px" style="margin-left: 2px"
/> />
</a-button> </j-button>
</a-col> </j-col>
<a-col :span="24"> <j-col :span="24">
<p style="margin-top: 20px">属性映射</p> <p style="margin-top: 20px">属性映射</p>
<a-collapse <j-collapse
v-if="modelRef.propertyMappings.length" v-if="modelRef.propertyMappings.length"
:activeKey="propertyActiveKey" :activeKey="propertyActiveKey"
@change="onPropertyCollChange" @change="onPropertyCollChange"
> >
<a-collapse-panel <j-collapse-panel
v-for="( v-for="(
item, index item, index
) in modelRef.propertyMappings" ) in modelRef.propertyMappings"
@ -302,9 +302,9 @@
type="DeleteOutlined" type="DeleteOutlined"
@click="delPropertyItem(index)" @click="delPropertyItem(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="DuerOS属性" label="DuerOS属性"
:name="[ :name="[
'propertyMappings', 'propertyMappings',
@ -317,7 +317,7 @@
'请选择DuerOS属性', '请选择DuerOS属性',
}" }"
> >
<a-select <j-select
placeholder="请选择DuerOS属性" placeholder="请选择DuerOS属性"
v-model:value=" v-model:value="
item.source item.source
@ -327,7 +327,7 @@
filterOption filterOption
" "
> >
<a-select-option <j-select-option
v-for="i in getDuerOSProperties( v-for="i in getDuerOSProperties(
item.source, item.source,
)" )"
@ -335,13 +335,13 @@
:value="i.id" :value="i.id"
>{{ >{{
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-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
label="平台属性" label="平台属性"
:name="[ :name="[
'propertyMappings', 'propertyMappings',
@ -354,7 +354,7 @@
'请选择平台属性', '请选择平台属性',
}" }"
> >
<a-select <j-select
placeholder="请选择平台属性" placeholder="请选择平台属性"
v-model:value=" v-model:value="
item.target item.target
@ -365,7 +365,7 @@
filterOption filterOption
" "
> >
<a-select-option <j-select-option
v-for="i in getProductProperties( v-for="i in getProductProperties(
item.target, item.target,
)" )"
@ -373,17 +373,17 @@
:value="item.id" :value="item.id"
>{{ >{{
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-col>
<a-col :span="24"> <j-col :span="24">
<a-button <j-button
type="dashed" type="dashed"
style="width: 100%; margin-top: 10px" style="width: 100%; margin-top: 10px"
@click="addPropertyItem" @click="addPropertyItem"
@ -392,10 +392,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="{
@ -403,16 +403,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"
@ -423,12 +423,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>
@ -442,9 +442,10 @@ import {
detail, detail,
} from '@/api/northbound/dueros'; } from '@/api/northbound/dueros';
import _ from 'lodash'; import _ from 'lodash';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
import { useMenuStore } from '@/store/menu';
const router = useRouter(); const menuStory = useMenuStore();
const route = useRoute(); const route = useRoute();
const formRef = ref(); const formRef = ref();
@ -640,7 +641,8 @@ const saveBtn = async () => {
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
formRef.value.resetFields(); formRef.value.resetFields();
router.push('/iot/northbound/DuerOS/'); menuStory.jumpPage('Northbound/DuerOS');
} }
} }
}) })

View File

@ -1,11 +1,11 @@
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="northbound-dueros" target="northbound-dueros"
@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,18 +45,18 @@
> >
{{ 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?.productName }}</div> <div>{{ slotProps?.productName }}</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?.applianceType?.text }}</div> <div>{{ slotProps?.applianceType?.text }}</div>
</a-col> </j-col>
</a-row> </j-row>
</template> </template>
<template #actions="item"> <template #actions="item">
<PermissionButton <PermissionButton
@ -81,7 +81,7 @@
</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)"
/> />
@ -90,7 +90,7 @@
{{ slotProps.applianceType.text }} {{ slotProps.applianceType.text }}
</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"
@ -109,9 +109,9 @@
<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>
@ -124,15 +124,13 @@ import {
queryProductList, queryProductList,
queryTypes, queryTypes,
} from '@/api/northbound/dueros'; } from '@/api/northbound/dueros';
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();
const statusMap = new Map(); const statusMap = new Map();

View File

@ -16,7 +16,7 @@
<div class="dialog-box"> <div class="dialog-box">
<div class="dialog-header"> <div class="dialog-header">
<div class="dialog-title"> <div class="dialog-title">
<a-badge <j-badge
:color=" :color="
statusColor.get( statusColor.get(
item.error ? 'error' : 'success', item.error ? 'error' : 'success',
@ -41,7 +41,7 @@
class="dialog-editor" class="dialog-editor"
v-if="visible.includes(item.key)" v-if="visible.includes(item.key)"
> >
<a-textarea autoSize :bordered="false" :value="item?.detail" /> <j-textarea autoSize :bordered="false" :value="item?.detail" />
</div> </div>
</div> </div>
</div> </div>
@ -50,8 +50,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const operationMap = new Map();
import moment from 'moment'; import moment from 'moment';
const operationMap = new Map();
operationMap.set('connection', '连接'); operationMap.set('connection', '连接');
operationMap.set('auth', '权限验证'); operationMap.set('auth', '权限验证');
operationMap.set('decode', '解码'); operationMap.set('decode', '解码');
@ -80,19 +81,9 @@ const getDetail = (item: any) => {
visible.value.splice(index, 1); visible.value.splice(index, 1);
} }
}; };
watchEffect(() => {
console.log(props.data)
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
// @import 'ant-design-vue/es/style/themes/default.less';
// :root {
// --dialog-primary-color: @primary-color;
// }
.dialog-item { .dialog-item {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@ -1,5 +1,5 @@
<template> <template>
<a-table <j-table
rowKey="id" rowKey="id"
:columns="columns" :columns="columns"
:data-source="dataSource" :data-source="dataSource"
@ -36,12 +36,11 @@
</template> </template>
</div> </div>
</template> </template>
</a-table> </j-table>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PropType } from "vue-demi"; import { PropType } from "vue";
type Emits = { type Emits = {
(e: 'update:modelValue', data: Record<string, any>[]): void; (e: 'update:modelValue', data: Record<string, any>[]): void;

View File

@ -1,110 +1,110 @@
<template> <template>
<div class="function"> <div class="function">
<a-form :layout="'vertical'" ref="formRef" :model="modelRef"> <j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<a-row :gutter="24"> <j-row :gutter="24">
<a-col :span="6"> <j-col :span="6">
<a-form-item <j-form-item
name="type" name="type"
:rules="{ :rules="{
required: true, required: true,
message: '请选择', message: '请选择',
}" }"
> >
<a-select <j-select
placeholder="请选择" placeholder="请选择"
v-model:value="modelRef.type" v-model:value="modelRef.type"
show-search show-search
:filter-option="filterOption" :filter-option="filterOption"
> >
<a-select-option value="READ_PROPERTY" <j-select-option value="READ_PROPERTY"
>读取属性</a-select-option >读取属性</j-select-option
> >
<a-select-option value="WRITE_PROPERTY" <j-select-option value="WRITE_PROPERTY"
>修改属性</a-select-option >修改属性</j-select-option
> >
<a-select-option value="INVOKE_FUNCTION" <j-select-option value="INVOKE_FUNCTION"
>调用功能</a-select-option >调用功能</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col <j-col
:span="6" :span="6"
v-if=" v-if="
['READ_PROPERTY', 'WRITE_PROPERTY'].includes( ['READ_PROPERTY', 'WRITE_PROPERTY'].includes(
modelRef.type, modelRef?.type || '',
) )
" "
> >
<a-form-item <j-form-item
name="properties" name="properties"
:rules="{ :rules="{
required: true, required: true,
message: '请选择属性', message: '请选择属性',
}" }"
> >
<a-select <j-select
placeholder="请选择属性" placeholder="请选择属性"
v-model:value="modelRef.properties" v-model:value="modelRef.properties"
show-search show-search
:filter-option="filterOption" :filter-option="filterOption"
> >
<a-select-option <j-select-option
v-for="i in metadata?.properties || []" v-for="i in metadata?.properties || []"
:key="i.id" :key="i.id"
:value="i.id" :value="i.id"
:label="i.name" :label="i.name"
>{{ i.name }}</a-select-option >{{ i.name }}</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="6" v-if="modelRef.type === 'WRITE_PROPERTY'"> <j-col :span="6" v-if="modelRef.type === 'WRITE_PROPERTY'">
<a-form-item <j-form-item
name="propertyValue" name="propertyValue"
:rules="{ :rules="{
required: true, required: true,
message: '请输入值', message: '请输入值',
}" }"
> >
<a-input v-model:value="modelRef.propertyValue" /> <j-input v-model:value="modelRef.propertyValue" />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="6" v-if="modelRef.type === 'INVOKE_FUNCTION'"> <j-col :span="6" v-if="modelRef.type === 'INVOKE_FUNCTION'">
<a-form-item <j-form-item
name="function" name="function"
:rules="{ :rules="{
required: true, required: true,
message: '请选择功能', message: '请选择功能',
}" }"
> >
<a-select <j-select
placeholder="请选择功能" placeholder="请选择功能"
v-model:value="modelRef.function" v-model:value="modelRef.function"
show-search show-search
:filter-option="filterOption" :filter-option="filterOption"
@change="funcChange" @change="funcChange"
> >
<a-select-option <j-select-option
v-for="i in metadata?.functions || []" v-for="i in metadata?.functions || []"
:key="i.id" :key="i.id"
:value="i.id" :value="i.id"
:label="i.name" :label="i.name"
>{{ i.name }}</a-select-option >{{ i.name }}</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col :span="4"> <j-col :span="4">
<a-button type="primary" @click="saveBtn">发送</a-button> <j-button type="primary" @click="saveBtn">发送</j-button>
</a-col> </j-col>
<a-col <j-col
:span="24" :span="24"
v-if=" v-if="
modelRef.type === 'INVOKE_FUNCTION' && modelRef.function && modelRef.inputs.length modelRef.type === 'INVOKE_FUNCTION' && modelRef.function && modelRef.inputs.length
" "
> >
<a-form-item <j-form-item
name="inputs" name="inputs"
label="参数列表" label="参数列表"
:rules="{ :rules="{
@ -113,10 +113,10 @@
}" }"
> >
<EditTable v-model="modelRef.inputs" /> <EditTable v-model="modelRef.inputs" />
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
</a-form> </j-form>
</div> </div>
</template> </template>

View File

@ -1,3 +1,106 @@
<template> <template>
log <div class="log-item" :key="data.id">
</template> <div class="log-card">
<div class="log-icon" @click="visible = !visible">
<AIcon :type="visible ? 'DownOutlined' : 'RightOutlined'" />
</div>
<div class="log-box">
<div class="log-header">
<div class="log-title">
<j-tag color="error">ERROR</j-tag>
{{ operationMap.get(data.operation) }}
</div>
<div class="log-time">
{{ dayjs(data.endTime).format('YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
<div className="log-editor" v-if="visible">
<j-textarea
autoSize
:bordered="false"
:value="data?.detail"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs';
const operationMap = new Map();
operationMap.set('connection', '连接');
operationMap.set('auth', '权限验证');
operationMap.set('decode', '解码');
operationMap.set('encode', '编码');
operationMap.set('request', '请求');
operationMap.set('response', '响应');
operationMap.set('downstream', '下行消息');
operationMap.set('upstream', '上行消息');
const visible = ref<boolean>(false);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
</script>
<style lang="less" scoped>
.log-item {
display: flex;
justify-content: flex-start;
width: 100%;
padding-bottom: 12px;
.log-card {
display: flex;
width: 100%;
background-color: #fff;
.log-icon {
margin-right: 10px;
color: rgba(0, 0, 0, 0.75);
font-weight: 500;
font-size: 12px;
}
.log-box {
display: flex;
flex-direction: column;
width: 100%;
.log-header {
.log-title {
color: rgba(0, 0, 0, 0.75);
font-weight: 700;
font-size: 14px;
}
.log-time {
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
}
}
.log-editor {
width: 100%;
margin-top: 10px;
color: rgba(0, 0, 0, 0.75);
textarea {
color: black !important;
background-color: #fafafa !important;
}
textarea::-webkit-scrollbar {
width: 5px !important;
}
}
}
}
}
</style>

View File

@ -1,20 +1,20 @@
<template> <template>
<a-row :gutter="24"> <j-row :gutter="24">
<a-col :span="16"> <j-col :span="16">
<a-row :gutter="24" style="margin-bottom: 20px"> <j-row :gutter="24" style="margin-bottom: 20px">
<a-col :span="12" v-for="item in messageArr" :key="item"> <j-col :span="12" v-for="item in messageArr" :key="item">
<div <div
:style="messageStyleMap.get(item.status)" :style="messageStyleMap.get(item.status)"
class="message-status" class="message-status"
> >
<a-badge <j-badge
:status="messageStatusMap.get(item.status)" :status="messageStatusMap.get(item.status)"
style="margin-right: 5px" style="margin-right: 5px"
/> />
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
</div> </div>
</a-col> </j-col>
</a-row> </j-row>
<div> <div>
<TitleComponent data="调试" /> <TitleComponent data="调试" />
<div class="content"> <div class="content">
@ -26,8 +26,8 @@
</div> </div>
<div><Function /></div> <div><Function /></div>
</div> </div>
</a-col> </j-col>
<a-col :span="8"> <j-col :span="8">
<div class="right-log"> <div class="right-log">
<TitleComponent data="日志" /> <TitleComponent data="日志" />
<div :style="{ marginTop: '10px' }"> <div :style="{ marginTop: '10px' }">
@ -38,11 +38,11 @@
:key="item.key" :key="item.key"
/> />
</template> </template>
<a-empty v-else /> <j-empty v-else />
</div> </div>
</div> </div>
</a-col> </j-col>
</a-row> </j-row>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,7 +1,6 @@
import { Badge, Descriptions, Modal, Tooltip } from "ant-design-vue" import { Badge, Descriptions, Modal, Tooltip, AIcon, DescriptionsItem } from "jetlinks-ui-components"
import TitleComponent from '@/components/TitleComponent/index.vue' import TitleComponent from '@/components/TitleComponent/index.vue'
import styles from './index.module.less' import styles from './index.module.less'
import AIcon from "@/components/AIcon";
import _ from "lodash"; import _ from "lodash";
const DiagnosticAdvice = defineComponent({ const DiagnosticAdvice = defineComponent({
@ -42,11 +41,11 @@ const DiagnosticAdvice = defineComponent({
<div style={{ marginTop: 15 }}> <div style={{ marginTop: 15 }}>
<TitleComponent data="连接信息" /> <TitleComponent data="连接信息" />
<Descriptions column={2}> <Descriptions column={2}>
<Descriptions.Item span={1} label="设备ID"> <DescriptionsItem span={1} label="设备ID">
{data?.info?.id || ''} {data?.info?.id || ''}
</Descriptions.Item> </DescriptionsItem>
{data?.info?.address?.length > 0 && ( {data?.info?.address?.length > 0 && (
<Descriptions.Item span={1} label="连接地址"> <DescriptionsItem span={1} label="连接地址">
<Tooltip <Tooltip
placement="topLeft" placement="topLeft"
title={ title={
@ -69,11 +68,11 @@ const DiagnosticAdvice = defineComponent({
))} ))}
</div> </div>
</Tooltip> </Tooltip>
</Descriptions.Item> </DescriptionsItem>
)} )}
{(_.flatten(_.map(data?.info?.config, 'properties')) || []).map((item: any, index: number) => ( {(_.flatten(_.map(data?.info?.config, 'properties')) || []).map((item: any, index: number) => (
<Descriptions.Item <DescriptionsItem
key={index} key={index}
span={1} span={1}
label={ label={
@ -90,7 +89,7 @@ const DiagnosticAdvice = defineComponent({
} }
> >
{data?.info?.configValue[item?.property] || ''} {data?.info?.configValue[item?.property] || ''}
</Descriptions.Item> </DescriptionsItem>
))} ))}
</Descriptions> </Descriptions>
</div> </div>

View File

@ -1,7 +1,6 @@
import AIcon from "@/components/AIcon";
import { useInstanceStore } from "@/store/instance"; import { useInstanceStore } from "@/store/instance";
import { useMenuStore } from "@/store/menu"; import { useMenuStore } from "@/store/menu";
import { Button, Descriptions, Modal } from "ant-design-vue" import { AIcon, Button, Modal, Descriptions, DescriptionsItem, Space } from "jetlinks-ui-components"
import styles from './index.module.less' import styles from './index.module.less'
const ManualInspection = defineComponent({ const ManualInspection = defineComponent({
@ -39,12 +38,12 @@ const ManualInspection = defineComponent({
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
<Descriptions title={data?.data?.name} layout="vertical" bordered> <Descriptions title={data?.data?.name} layout="vertical" bordered>
{(data?.data?.properties || []).map((item: any) => ( {(data?.data?.properties || []).map((item: any) => (
<Descriptions.Item <DescriptionsItem
key={item.property} key={item.property}
label={`${item.name}${item?.description ? `(${item.description})` : ''}`} label={`${item.name}${item?.description ? `(${item.description})` : ''}`}
> >
{data?.configuration[item.property] || ''} {data?.configuration[item.property] || ''}
</Descriptions.Item> </DescriptionsItem>
))} ))}
</Descriptions> </Descriptions>
</div> </div>
@ -80,30 +79,30 @@ const ManualInspection = defineComponent({
<Descriptions title={data?.data?.name} layout="vertical" bordered> <Descriptions title={data?.data?.name} layout="vertical" bordered>
{data.configuration?.provider === 'OneNet' ? ( {data.configuration?.provider === 'OneNet' ? (
<> <>
<Descriptions.Item label={'接口地址'}> <DescriptionsItem label={'接口地址'}>
{data?.configuration?.configuration?.apiAddress || ''} {data?.configuration?.configuration?.apiAddress || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'apiKey'}> <DescriptionsItem label={'apiKey'}>
{data?.configuration?.configuration?.apiKey || ''} {data?.configuration?.configuration?.apiKey || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'通知Token'}> <DescriptionsItem label={'通知Token'}>
{data?.configuration?.configuration?.validateToken || ''} {data?.configuration?.configuration?.validateToken || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'aesKey'}> <DescriptionsItem label={'aesKey'}>
{data?.configuration?.configuration?.aesKey || ''} {data?.configuration?.configuration?.aesKey || ''}
</Descriptions.Item> </DescriptionsItem>
</> </>
) : ( ) : (
<> <>
<Descriptions.Item label={'接口地址'}> <DescriptionsItem label={'接口地址'}>
{data?.configuration?.configuration?.apiAddress || ''} {data?.configuration?.configuration?.apiAddress || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'appKey'}> <DescriptionsItem label={'appKey'}>
{data?.configuration?.configuration?.appKey || ''} {data?.configuration?.configuration?.appKey || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'appSecret'}> <DescriptionsItem label={'appSecret'}>
{data?.configuration?.configuration?.appSecret || ''} {data?.configuration?.configuration?.appSecret || ''}
</Descriptions.Item> </DescriptionsItem>
</> </>
)} )}
</Descriptions> </Descriptions>
@ -140,45 +139,45 @@ const ManualInspection = defineComponent({
<Descriptions title={data?.data?.name} layout="vertical" bordered> <Descriptions title={data?.data?.name} layout="vertical" bordered>
{data?.configuration?.configuration?.shareCluster ? ( {data?.configuration?.configuration?.shareCluster ? (
<> <>
<Descriptions.Item label={'SIP 域'}> <DescriptionsItem label={'SIP 域'}>
{data?.configuration?.configuration?.domain || ''} {data?.configuration?.configuration?.domain || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'SIP ID'}> <DescriptionsItem label={'SIP ID'}>
{data?.configuration?.configuration?.sipId || ''} {data?.configuration?.configuration?.sipId || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'集群'}> <DescriptionsItem label={'集群'}>
{data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'} {data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'SIP 地址'}> <DescriptionsItem label={'SIP 地址'}>
{`${data?.configuration?.configuration?.hostPort?.host}:${data?.configuration?.configuration?.hostPort?.port}`} {`${data?.configuration?.configuration?.hostPort?.host}:${data?.configuration?.configuration?.hostPort?.port}`}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'公网 Host'}> <DescriptionsItem label={'公网 Host'}>
{`${data?.configuration?.configuration?.hostPort?.publicHost}:${data?.configuration?.configuration?.hostPort?.publicPort}`} {`${data?.configuration?.configuration?.hostPort?.publicHost}:${data?.configuration?.configuration?.hostPort?.publicPort}`}
</Descriptions.Item> </DescriptionsItem>
</> </>
) : ( ) : (
<> <>
<Descriptions.Item label={'SIP 域'}> <DescriptionsItem label={'SIP 域'}>
{data?.configuration?.configuration?.domain || ''} {data?.configuration?.configuration?.domain || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'SIP ID'}> <DescriptionsItem label={'SIP ID'}>
{data?.configuration?.configuration?.sipId || ''} {data?.configuration?.configuration?.sipId || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'集群'}> <DescriptionsItem label={'集群'}>
{data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'} {data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'}
</Descriptions.Item> </DescriptionsItem>
{data?.configuration?.configuration?.cluster.map((i: any, it: number) => ( {data?.configuration?.configuration?.cluster.map((i: any, it: number) => (
<div key={it}> <div key={it}>
<div>{it + 1}</div> <div>{it + 1}</div>
<Descriptions.Item label={'节点名称'}> <DescriptionsItem label={'节点名称'}>
{i?.clusterNodeId || ''} {i?.clusterNodeId || ''}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'SIP 地址'}> <DescriptionsItem label={'SIP 地址'}>
{`${i.host}:${i?.port}`} {`${i.host}:${i?.port}`}
</Descriptions.Item> </DescriptionsItem>
<Descriptions.Item label={'公网 Host'}> <DescriptionsItem label={'公网 Host'}>
{`${i?.publicHost}:${i?.publicPort}`} {`${i?.publicHost}:${i?.publicPort}`}
</Descriptions.Item> </DescriptionsItem>
</div> </div>
))} ))}
</> </>
@ -207,20 +206,29 @@ const ManualInspection = defineComponent({
title="人工检查" title="人工检查"
visible visible
width={1000} width={1000}
cancelText="去修改"
okText="确认无误"
onOk={() => { onOk={() => {
emit('save', data) emit('save', data)
}} }}
onCancel={() => { onCancel={() => {
if (data.type === 'device') { emit('close')
instanceStore.tabActiveKey = 'Info' }}
} else if (data.type === 'product') { v-slots={{
menuStory.jumpPage('device/Product/Detail', { id: data.productId, tab: 'access' }); footer: <Space>
} else { <Button onClick={() => {
menuStory.jumpPage('link/AccessConfig/Detail', { id: data.configuration?.id }); if (data.type === 'device') {
} instanceStore.tabActiveKey = 'Info'
}}> } else if (data.type === 'product') {
menuStory.jumpPage('device/Product/Detail', { id: data.productId, tab: 'access' });
} else {
menuStory.jumpPage('link/AccessConfig/Detail', { id: data.configuration?.id });
}
}}></Button>
<Button onClick={() => {
emit('save', data)
}}></Button>
</Space>
}}
>
<div style={{ display: 'flex' }}>{dataRender()}</div> <div style={{ display: 'flex' }}>{dataRender()}</div>
</Modal> </Modal>
} }

View File

@ -1,4 +1,4 @@
import { Badge, Button, message, Popconfirm, Space } from "ant-design-vue" import { Badge, Button, message, Popconfirm, Space } from "jetlinks-ui-components"
import TitleComponent from '@/components/TitleComponent/index.vue' import TitleComponent from '@/components/TitleComponent/index.vue'
import styles from './index.module.less' import styles from './index.module.less'
import type { ListProps } from './util' import type { ListProps } from './util'
@ -13,6 +13,7 @@ import ManualInspection from './ManualInspection'
import { deployDevice } from "@/api/initHome" import { deployDevice } from "@/api/initHome"
import PermissionButton from '@/components/PermissionButton/index.vue' import PermissionButton from '@/components/PermissionButton/index.vue'
import { useMenuStore } from "@/store/menu" import { useMenuStore } from "@/store/menu"
import BindParentDevice from '../../components/BindParentDevice/index.vue'
type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel' type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel'
@ -1691,6 +1692,102 @@ const Status = defineComponent({
}} }}
/> />
)} )}
{
bindParentVisible && (
<BindParentDevice
data={device.value}
onCancel={() => {
bindParentVisible.value = false
}}
onOk={async (parentId: string) => {
let item: ListProps | undefined = undefined;
const response = await detail(parentId);
if (response.status === 200) {
if (response?.result?.state?.value === 'notActive') {
item = {
key: 'parent-device',
name: '网关父设备',
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
status: 'error',
text: '异常',
info: (
<div>
<div class={styles.infoItem}>
<Badge
status="default"
text={
<span>
<PermissionButton
hasPermission="device/Product:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await _deploy(response?.result?.id || '');
if (resp.status === 200) {
message.success('操作成功!');
list.value = modifyArrayList(
list.value,
{
key: 'parent-device',
name: '网关父设备',
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
status: 'success',
text: '正常',
info: null,
},
);
}
}
}}
>
</PermissionButton>
</span>
}
/>
</div>
</div>
),
};
} else if (response?.state?.value === 'online') {
item = {
key: 'parent-device',
name: '网关父设备',
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
status: 'success',
text: '正常',
info: null,
};
} else {
item = {
key: 'parent-device',
name: '网关父设备',
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
status: 'error',
text: '异常',
info: (
<div>
<div class={styles.infoItem}>
<Badge
status="default"
text={<span>线</span>}
/>
</div>
</div>
),
};
}
if (item) {
list.value = modifyArrayList(unref(list), item);
}
instanceStore.current.parentId = parentId;
bindParentVisible.value = false
}
}}
/>
)
}
</div> </div>
}, },
}) })

View File

@ -1,5 +1,5 @@
<template> <template>
<a-card> <j-card>
<div class="diagnose"> <div class="diagnose">
<div class="diagnose-header" :style="{background: headerColorMap.get(topState)}"> <div class="diagnose-header" :style="{background: headerColorMap.get(topState)}">
<div class="diagnose-top"> <div class="diagnose-top">
@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<div class="diagnose-progress"> <div class="diagnose-progress">
<a-progress <j-progress
:percent="percent" :percent="percent"
:showInfo="false" :showInfo="false"
size="small" size="small"
@ -38,7 +38,7 @@
<Status v-show="activeKey !== 'message'" :providerType="providerType" @countChange="countChange" @percentChange="percentChange" @stateChange="stateChange" /> <Status v-show="activeKey !== 'message'" :providerType="providerType" @countChange="countChange" @percentChange="percentChange" @stateChange="stateChange" />
</div> </div>
</div> </div>
</a-card> </j-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -19,7 +19,7 @@
</div> </div>
</template> </template>
<j-form layout="vertical" ref="formRef" :model="modelRef"> <j-form layout="vertical" ref="formRef" :model="modelRef">
<template v-for="(item, index) in props.config" :key="index"> <template v-for="(item, index) in (props.config || [])" :key="index">
<j-form-item <j-form-item
:name="item.property" :name="item.property"
v-for="i in item.properties" v-for="i in item.properties"
@ -54,7 +54,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { modify } from '@/api/device/instance'; import { modify } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance'; import { useInstanceStore } from '@/store/instance';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
const emit = defineEmits(['close', 'save']); const emit = defineEmits(['close', 'save']);

View File

@ -97,7 +97,7 @@ import {
_deploy, _deploy,
configurationReset, configurationReset,
} from '@/api/device/instance'; } from '@/api/device/instance';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
import Save from './Save.vue'; import Save from './Save.vue';
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();

View File

@ -46,7 +46,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { queryUserListNoPaging, saveRelations } from '@/api/device/instance'; import { queryUserListNoPaging, saveRelations } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance'; import { useInstanceStore } from '@/store/instance';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
const emit = defineEmits(['close', 'save']); const emit = defineEmits(['close', 'save']);

View File

@ -9,7 +9,7 @@
<j-table <j-table
rowKey="id" rowKey="id"
:columns="columns" :columns="columns"
:datj-source="dataSource" :data-source="dataSource"
bordered bordered
:pagination="false" :pagination="false"
> >
@ -49,7 +49,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useInstanceStore } from '@/store/instance'; import { useInstanceStore } from '@/store/instance';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
import _ from 'lodash'; import _ from 'lodash';
import { saveTags, delTags } from '@/api/device/instance' import { saveTags, delTags } from '@/api/device/instance'

View File

@ -1,9 +1,11 @@
<template> <template>
<j-card> <j-card>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="device-instance-log" target="device-instance-log"
@search="handleSearch" @search="handleSearch"
type="simple"
class="search"
/> />
<JProTable <JProTable
ref="instanceRefLog" ref="instanceRefLog"
@ -12,6 +14,7 @@
model="TABLE" model="TABLE"
:defaultParams="{ sorts: [{ name: 'timestamp', order: 'desc' }] }" :defaultParams="{ sorts: [{ name: 'timestamp', order: 'desc' }] }"
:params="params" :params="params"
:bodyStyle="{ padding: 0 }"
> >
<template #type="slotProps"> <template #type="slotProps">
{{ slotProps?.type?.text }} {{ slotProps?.type?.text }}
@ -150,4 +153,7 @@ const handleSearch = (_params: any) => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.search {
padding: 0;
}
</style> </style>

View File

@ -1,28 +1,30 @@
<template> <template>
<Search :columns="columns" target="device-instance-running-events" /> <j-advanced-search class="search" type="simple" :columns="columns" target="device-instance-running-events" @search="handleSearch" />
<JTable <JProTable
ref="eventsRef" ref="eventsRef"
:columns="columns" :columns="columns"
:request="_getEventList" :request="_getEventList"
model="TABLE" model="TABLE"
:bodyStyle="{ padding: '0 24px' }" :params="params"
:bodyStyle="{ padding: '0 0 0 24px' }"
> >
<template #timestamp="slotProps"> <template #timestamp="slotProps">
{{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-button type="link" @click="detail(slotProps)"> <j-button type="link" @click="detail(slotProps)">
<AIcon type="SearchOutlined" /> <AIcon type="SearchOutlined" />
</a-button> </j-button>
</template> </template>
</JTable> </JProTable>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import moment from 'moment'; import dayjs from 'dayjs';
import { getEventList } from '@/api/device/instance'; import { getEventList } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance'; import { useInstanceStore } from '@/store/instance';
import { Modal } from 'ant-design-vue'; import { Modal } from 'jetlinks-ui-components';
import JsonViewer from 'vue-json-viewer';
const events = defineProps({ const events = defineProps({
data: { data: {
@ -51,11 +53,11 @@ const columns = ref<Record<string, any>>([
]); ]);
const params = ref<Record<string, any>>({}); const params = ref<Record<string, any>>({});
const _getEventList = () => const _getEventList = (_params: any) =>
getEventList( getEventList(
instanceStore.current.id || '', instanceStore.current.id || '',
events.data.id || '', events.data.id || '',
params.value, _params
); );
watchEffect(() => { watchEffect(() => {
@ -78,12 +80,25 @@ watchEffect(() => {
} }
}); });
const detail = () => { const handleSearch = (_params: any) => {
params.value = _params;
};
const detail = (_info: any) => {
Modal.info({ Modal.info({
title: () => '详情', title: () => '详情',
width: 850, width: 850,
content: () => h('div', {}, [h('p', '暂未开发')]), content: () => h('JsonViewer', {
'expand-depth': 5,
value: _info
}),
okText: '关闭', okText: '关闭',
}); });
}; };
</script> </script>
<style lang="less" scoped>
.search {
padding: 0 0 0 24px;
}
</style>

View File

@ -1,36 +1,36 @@
<template> <template>
<a-spin :spinning="loading"> <j-spin :spinning="loading">
<div> <div>
<a-space> <j-space>
<div> <div>
统计周期 统计周期
<a-select v-model:value="cycle" style="width: 120px"> <j-select v-model:value="cycle" style="width: 120px">
<a-select-option value="*" v-if="_type" <j-select-option value="*" v-if="_type"
>实际值</a-select-option >实际值</j-select-option
> >
<a-select-option value="1m">按分钟统计</a-select-option> <j-select-option value="1m">按分钟统计</j-select-option>
<a-select-option value="1h">按小时统计</a-select-option> <j-select-option value="1h">按小时统计</j-select-option>
<a-select-option value="1d">按天统计</a-select-option> <j-select-option value="1d">按天统计</j-select-option>
<a-select-option value="1w">按周统计</a-select-option> <j-select-option value="1w">按周统计</j-select-option>
<a-select-option value="1M">按月统计</a-select-option> <j-select-option value="1M">按月统计</j-select-option>
</a-select> </j-select>
</div> </div>
<div v-if="cycle !== '*' && _type"> <div v-if="cycle !== '*' && _type">
统计规则 统计规则
<a-select v-model:value="agg" style="width: 120px"> <j-select v-model:value="agg" style="width: 120px">
<a-select-option value="AVG">平均值</a-select-option> <j-select-option value="AVG">平均值</j-select-option>
<a-select-option value="MAX">最大值</a-select-option> <j-select-option value="MAX">最大值</j-select-option>
<a-select-option value="MIN">最小值</a-select-option> <j-select-option value="MIN">最小值</j-select-option>
<a-select-option value="COUNT">总数</a-select-option> <j-select-option value="COUNT">总数</j-select-option>
</a-select> </j-select>
</div> </div>
</a-space> </j-space>
</div> </div>
<div style="width: 100%; height: 500px"> <div style="width: 100%; height: 500px">
<Chart :options="options" v-if="chartsList.length" /> <Chart :options="options" v-if="chartsList.length" />
<JEmpty v-else /> <JEmpty v-else />
</div> </div>
</a-spin> </j-spin>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,17 +1,17 @@
<template> <template>
<a-spin :spinning="loading"> <j-spin :spinning="loading">
<div style="position: relative"> <div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999"> <div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space> <j-space>
<a-button type="primary" @click="onStart">开始动画</a-button> <j-button type="primary" @click="onStart">开始动画</j-button>
<a-button type="primary" @click="onStop">停止动画</a-button> <j-button type="primary" @click="onStop">停止动画</j-button>
</a-space> </j-space>
</div> </div>
</div> </div>
<AMapComponent style="height: 500px"> <AMapComponent style="height: 500px">
<PathSimplifier :pathData="geoList" ref="amapPath"></PathSimplifier> <PathSimplifier :pathData="geoList" ref="amapPath"></PathSimplifier>
</AMapComponent> </AMapComponent>
</a-spin> </j-spin>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<a-table <j-table
:columns="columns" :columns="columns"
size="small" size="small"
rowKey="id" rowKey="id"
@ -11,7 +11,7 @@
pageSize: dataSource?.pageSize || 10, pageSize: dataSource?.pageSize || 10,
showSizeChanger: true, showSizeChanger: true,
total: dataSource?.total || 0, total: dataSource?.total || 0,
pageSizeOptions: [5, 10, 20, 50], pageSizeOptions: ['8', '12', '24', '60', '100']
}" }"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
@ -26,8 +26,8 @@
/> />
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <j-space>
<a-button <j-button
v-if=" v-if="
showLoad || showLoad ||
(!getType(record?.value) && (!getType(record?.value) &&
@ -36,16 +36,16 @@
type="link" type="link"
@click="_download(record)" @click="_download(record)"
><AIcon type="DownloadOutlined" ><AIcon type="DownloadOutlined"
/></a-button> /></j-button>
<a-button type="link" @click="showDetail(record)" <j-button type="link" @click="showDetail(record)"
><AIcon type="SearchOutlined" ><AIcon type="SearchOutlined"
/></a-button> /></j-button>
</a-space> </j-space>
</template> </template>
</template> </template>
</a-table> </j-table>
</div> </div>
<a-modal <j-modal
title="详情" title="详情"
:visible="visible" :visible="visible"
@ok="visible = false" @ok="visible = false"
@ -60,13 +60,13 @@
:expand-depth="5" :expand-depth="5"
:value="current.formatValue" :value="current.formatValue"
/> />
<a-textarea <j-textarea
v-else-if="data?.valueType?.type === 'file'" v-else-if="data?.valueType?.type === 'file'"
:value="current.formatValue" :value="current.formatValue"
:row="3" :row="3"
/> />
<a-input v-else disabled :value="current.formatValue" /> <j-input v-else disabled :value="current.formatValue" />
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -90,7 +90,12 @@ const _props = defineProps({
}); });
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const dataSource = ref({}); const dataSource = ref({
pageIndex: 0,
pageSize: 10,
data: [],
total: 0
});
const current = ref<any>({}); const current = ref<any>({});
const visible = ref<boolean>(false); const visible = ref<boolean>(false);

View File

@ -1,22 +1,22 @@
<template> <template>
<a-space> <j-space>
<a-radio-group <j-radio-group
:value="radioValue" :value="radioValue"
button-style="solid" button-style="solid"
@change="onRadioChange" @change="onRadioChange"
> >
<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-group> </j-radio-group>
<a-range-picker <j-range-picker
show-time show-time
v-model:value="dateValue" v-model:value="dateValue"
:placeholder="['开始时间', '结束时间']" :placeholder="['开始时间', '结束时间']"
@change="onRangeChange" @change="onRangeChange"
:allowClear="false" :allowClear="false"
/> />
</a-space> </j-space>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,20 +1,20 @@
<template> <template>
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel"> <j-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div> <div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
<div> <div>
<a-tabs :destroyInactiveTabPane="true" v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto"> <j-tabs :destroyInactiveTabPane="true" v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
<a-tab-pane key="table" tab="列表"> <j-tab-pane key="table" tab="列表">
<Table :data="props.data" :time="_getTimes" /> <Table :data="props.data" :time="_getTimes" />
</a-tab-pane> </j-tab-pane>
<a-tab-pane key="charts" tab="图表"> <j-tab-pane key="charts" tab="图表">
<Charts :data="props.data" :time="_getTimes" /> <Charts :data="props.data" :time="_getTimes" />
</a-tab-pane> </j-tab-pane>
<a-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'"> <j-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'">
<PropertyAMap :data="props.data" :time="_getTimes" /> <PropertyAMap :data="props.data" :time="_getTimes" />
</a-tab-pane> </j-tab-pane>
</a-tabs> </j-tabs>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
:maskClosable="false" :maskClosable="false"
:visible="true" :visible="true"
title="编辑指标" title="编辑指标"
@ -7,17 +7,32 @@
@cancel="handleCancel" @cancel="handleCancel"
:confirmLoading="loading" :confirmLoading="loading"
> >
<a-alert message="场景联动页面可引用指标配置触发条件" type="warning" showIcon /> <j-alert
<a-form layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px"> message="场景联动页面可引用指标配置触发条件"
type="warning"
showIcon
/>
<j-form
layout="vertical"
ref="formRef"
:model="modelRef"
style="margin-top: 20px"
>
<template v-for="(item, index) in modelRef.metrics" :key="index"> <template v-for="(item, index) in modelRef.metrics" :key="index">
<a-row type="flex" justify="space-between" align="bottom"> <j-row type="flex" justify="space-between" :align="'bottom'">
<a-col :span="11"> <j-col :span="item.range ? 11 : 24">
<a-form-item <j-form-item
:rules="{ :rules="{
required: true, required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`, message: `${
}" ['date', 'boolean'].includes(
:name="['metrics', index, 'value', 0]" data?.valueType?.type,
)
? '选择'
: '输入'
}指标值`,
}"
:name="['metrics', index, 'value', 0]"
:label="item?.name || '指标值'" :label="item?.name || '指标值'"
> >
<ValueItem <ValueItem
@ -26,100 +41,131 @@
:options=" :options="
data.valueType?.type === 'boolean' data.valueType?.type === 'boolean'
? [ ? [
{ {
label: data.valueType?.trueText, label: data.valueType
value: String(data.valueType?.trueValue), ?.trueText,
}, value: String(
{ data.valueType?.trueValue,
label: data.valueType?.falseText, ),
value: String(data.valueType?.falseValue), },
}, {
] label: data.valueType
?.falseText,
value: String(
data.valueType
?.falseValue,
),
},
]
: undefined : undefined
" "
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
<template v-if="item.range"> <template v-if="item.range">
<a-col><div class="center-icon">~</div></a-col> <j-col><div class="center-icon">~</div></j-col>
<a-col :span="11"> <j-col :span="11">
<a-form-item <j-form-item
:name="['metrics', index, 'value', 1]" :name="['metrics', index, 'value', 1]"
:rules="{ :rules="{
required: true, required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`, message: `${
}" ['date', 'boolean'].includes(
data?.valueType?.type,
)
? '选择'
: '输入'
}指标值`,
}"
> >
<ValueItem <ValueItem
v-model:modelValue="item.value[1]" v-model:modelValue="item.value[1]"
:itemType="data.valueType?.type" :itemType="data.valueType?.type"
/> />
</a-form-item> </j-form-item>
</a-col> </j-col>
</template> </template>
</a-row> </j-row>
</template> </template>
</a-form> </j-form>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { queryMetric, saveMetric } from '@/api/device/instance' import { queryMetric, saveMetric } from '@/api/device/instance';
const emit = defineEmits(['close']); import { useInstanceStore } from '@/store/instance';
import { useInstanceStore } from "@/store/instance" import { message } from 'jetlinks-ui-components';
import { message } from 'ant-design-vue';
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => {} default: () => {},
} },
}) });
const loading = ref<boolean>(false) const emit = defineEmits(['close']);
const instanceStore = useInstanceStore()
const loading = ref<boolean>(false);
const instanceStore = useInstanceStore();
const formRef = ref(); const formRef = ref();
const modelRef = reactive({ const modelRef = reactive({
metrics: [] metrics: [],
}); });
const handleCancel = () => { const handleCancel = () => {
emit('close') emit('close');
} };
watch(() => props.data.id, (newVal) => { watch(
if(newVal && instanceStore.current.id){ () => props.data.id,
queryMetric(instanceStore.current.id, props.data.id).then(resp => { (newVal) => {
if (resp.status === 200) { if (newVal && instanceStore.current.id) {
if (Array.isArray(resp?.result) && resp?.result.length) { queryMetric(instanceStore.current.id, props.data.id).then(
const list = resp?.result.map((item: any) => { (resp) => {
const val = Array.isArray(item?.value) ? [item?.value] : item?.value?.split(',') if (resp.status === 200) {
return { if (
...item, Array.isArray(resp?.result) &&
value: val resp?.result.length
}; ) {
}); const list = resp?.result.map((item: any) => {
modelRef.metrics = list as any const val = Array.isArray(item?.value)
} else { ? [item?.value]
const type = props.data.valueType?.type; : item?.value?.split(',');
if (type === 'boolean') { return {
const list = props.data.expands?.metrics.map((item: any) => { ...item,
const value = (item?.value || {}).map((i: any) => String(i)) || {}; value: val,
return { };
...item, });
value, modelRef.metrics = list as any;
}; } else {
}); const type = props.data.valueType?.type;
modelRef.metrics = list || [] if (type === 'boolean') {
} else { const list = props.data.expands?.metrics.map(
modelRef.metrics = props.data.expands?.metrics || [] (item: any) => {
} const value =
} (item?.value || {}).map((i: any) =>
String(i),
) || {};
return {
...item,
value,
};
},
);
modelRef.metrics = list || [];
} else {
modelRef.metrics =
props.data.expands?.metrics || [];
}
}
}
},
);
} }
}) },
} { immediate: true, deep: true },
}, {immediate: true, deep: true}) );
const handleSave = () => { const handleSave = () => {
formRef.value formRef.value
@ -132,19 +178,23 @@ const handleSave = () => {
value: item.value.join(','), value: item.value.join(','),
}; };
}); });
const resp = await saveMetric(instanceStore.current.id || '', props.data.id || '', list).finally(() => { const resp = await saveMetric(
loading.value = false instanceStore.current.id || '',
}) props.data.id || '',
list,
).finally(() => {
loading.value = false;
});
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
emit('close') emit('close');
formRef.value.resetFields(); formRef.value.resetFields();
} }
}) })
.catch((err: any) => { .catch((err: any) => {
console.log('error', err); console.log('error', err);
}); });
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,17 +1,17 @@
<template> <template>
<a-card :hoverable="true" class="card-box"> <j-card :hoverable="true" class="card-box">
<!-- <a-spin :spinning="loading"> --> <!-- <j-spin :spinning="loading"> -->
<div class="card-container"> <div class="card-container">
<div class="header"> <div class="header">
<div class="title">{{ _props.data.name }}</div> <div class="title">{{ _props.data.name }}</div>
<div class="extra"> <div class="extra">
<a-space :size="16"> <j-space :size="16">
<template v-for="i in actions" :key="i.key"> <template v-for="i in actions" :key="i.key">
<a-tooltip <j-tooltip
v-bind="i.tooltip" v-bind="i.tooltip"
v-if="i.key !== 'edit'" v-if="i.key !== 'edit'"
> >
<a-button <j-button
style="padding: 0; margin: 0" style="padding: 0; margin: 0"
type="link" type="link"
:disabled="i.disabled" :disabled="i.disabled"
@ -21,8 +21,8 @@
:type="i.icon" :type="i.icon"
style="color: #323130; font-size: 12px" style="color: #323130; font-size: 12px"
/> />
</a-button> </j-button>
</a-tooltip> </j-tooltip>
<PermissionButton <PermissionButton
:disabled="i.disabled" :disabled="i.disabled"
v-else v-else
@ -38,7 +38,7 @@
/></template> /></template>
</PermissionButton> </PermissionButton>
</template> </template>
</a-space> </j-space>
</div> </div>
</div> </div>
<div class="value"> <div class="value">
@ -53,8 +53,8 @@
</div> </div>
</div> </div>
</div> </div>
<!-- </a-spin> --> <!-- </j-spin> -->
</a-card> </j-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
:maskClosable="false" :maskClosable="false"
:visible="true" :visible="true"
title="编辑" title="编辑"
@ -7,9 +7,9 @@
@cancel="handleCancel" @cancel="handleCancel"
:confirmLoading="loading" :confirmLoading="loading"
> >
<a-alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon /> <j-alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon />
<a-form :rules="rules" layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px"> <j-form :rules="rules" layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px">
<a-form-item name="propertyValue" :label="data?.name || '自定义属性'"> <j-form-item name="propertyValue" :label="data?.name || '自定义属性'">
<ValueItem <ValueItem
v-model:modelValue="modelRef.propertyValue" v-model:modelValue="modelRef.propertyValue"
:itemType="data?.valueType?.type || data?.dataType" :itemType="data?.valueType?.type || data?.dataType"
@ -24,16 +24,15 @@
: undefined : undefined
" "
/> />
</a-form-item> </j-form-item>
</a-form> </j-form>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { setProperty } from '@/api/device/instance' import { setProperty } from '@/api/device/instance'
const emit = defineEmits(['close']);
import { useInstanceStore } from "@/store/instance" import { useInstanceStore } from "@/store/instance"
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
const props = defineProps({ const props = defineProps({
data: { data: {
@ -42,6 +41,8 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['close']);
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const instanceStore = useInstanceStore() const instanceStore = useInstanceStore()

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
:maskClosable="false" :maskClosable="false"
width="600px" width="600px"
:visible="true" :visible="true"
@ -10,7 +10,7 @@
@cancel="handleCancel" @cancel="handleCancel"
> >
<template v-if="['.jpg', '.png'].includes(type)"> <template v-if="['.jpg', '.png'].includes(type)">
<a-image :src="value?.formatValue" /> <j-image :src="value?.formatValue" />
</template> </template>
<template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)"> <template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)">
</template> </template>
@ -20,7 +20,7 @@
:value="value?.formatValue" :value="value?.formatValue"
/> />
</template> </template>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,59 +1,110 @@
<template> <template>
<div class="value"> <div class="value">
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div> <div
v-if="value?.formatValue !== 0 && !value?.formatValue"
:class="valueClass"
>
--
</div>
<div v-else-if="_data.data?.valueType?.type === 'file'"> <div v-else-if="_data.data?.valueType?.type === 'file'">
<template v-if="data?.valueType?.fileType === 'base64'"> <template v-if="data?.valueType?.fileType === 'base64'">
<div :class="valueClass" v-if="!!getType(value?.formatValue)"> <div :class="valueClass" v-if="!!getType(value?.formatValue)">
<img :src="imgMap.get(_type)" @error="onError" /> <img :src="imgMap.get(_type)" @error="onError" />
</div> </div>
<div v-else :class="valueClass"> <div v-else :class="valueClass">
<img :src="imgMap.get('other')" /> <img :src="imgMap.get('other')" />
</div> </div>
</template>
<div v-else-if="data?.valueType?.fileType === 'Binary(二进制)'" :class="valueClass">
<img :src="imgMap.get('other')" />
</div>
<template v-else>
<template v-if="imgList.some((item) => value?.formatValue.includes(item))">
<div :class="valueClass" @click="getDetail('img')">
<img :src="value?.formatValue" @error="imgError" />
</div>
</template> </template>
<template v-else-if="videoList.some((item) => value?.formatValue.includes(item))"> <div
<div :class="valueClass" @click="getDetail('video')"> v-else-if="data?.valueType?.fileType === 'Binary(二进制)'"
<img :src="imgMap.get('video')" /> :class="valueClass"
</div> >
</template>
<template v-else-if="fileList.some((item) => value?.formatValue.includes(item))">
<div :class="valueClass">
<img :src="imgMap.get(fileList.find((item) => value?.formatValue.includes(item)).slice(1))" />
</div>
</template>
<template v-else>
<div :class="valueClass">
<img :src="imgMap.get('other')" /> <img :src="imgMap.get('other')" />
</div> </div>
<template v-else>
<template
v-if="
imgList.some((item) =>
value?.formatValue.includes(item),
)
"
>
<div :class="valueClass" @click="getDetail('img')">
<img :src="value?.formatValue" @error="imgError" />
</div>
</template>
<template
v-else-if="
videoList.some((item) =>
value?.formatValue.includes(item),
)
"
>
<div :class="valueClass" @click="getDetail('video')">
<img :src="imgMap.get('video')" />
</div>
</template>
<template
v-else-if="
fileList.some((item) =>
value?.formatValue.includes(item),
)
"
>
<div :class="valueClass">
<img
:src="
imgMap.get(
fileList
.find((item) =>
value?.formatValue.includes(item),
)
.slice(1),
)
"
/>
</div>
</template>
<template v-else>
<div :class="valueClass">
<img :src="imgMap.get('other')" />
</div>
</template>
</template> </template>
</template>
</div> </div>
<div v-else-if="_data.data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass"> <div
<img :src="imgMap.get('obj')" /> v-else-if="_data.data?.valueType?.type === 'object'"
@click="getDetail('obj')"
:class="valueClass"
>
<img :src="imgMap.get('obj')" />
</div> </div>
<div v-else-if="_data.data?.valueType?.type === 'geoPoint' || _data.data?.valueType?.type === 'array'" :class="valueClass"> <div
{{JSON.stringify(value?.formatValue)}} v-else-if="
_data.data?.valueType?.type === 'geoPoint' ||
_data.data?.valueType?.type === 'array'
"
:class="valueClass"
>
{{ JSON.stringify(value?.formatValue) }}
</div> </div>
<div v-else :class="valueClass"> <div v-else :class="valueClass">
{{String(value?.formatValue)}} {{ String(value?.formatValue) }}
</div> </div>
<ValueDetail v-if="visible" :type="_types" :value="value" @close="visible = false" /> <ValueDetail
v-if="visible"
:type="_types"
:value="value"
@close="visible = false"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getImage } from "@/utils/comm"; import { getImage } from '@/utils/comm';
import { message } from "ant-design-vue"; import { message } from 'jetlinks-ui-components';
import ValueDetail from './ValueDetail.vue' import ValueDetail from './ValueDetail.vue';
import {getType, imgMap, imgList, videoList, fileList} from './index' import { getType, imgMap, imgList, videoList, fileList } from './index';
const _data = defineProps({ const _data = defineProps({
data: { data: {
@ -62,88 +113,99 @@ const _data = defineProps({
}, },
value: { value: {
type: Object, type: Object,
default: () => {} default: () => {},
}, },
type: { type: {
type: String, type: String,
default: 'card' default: 'card',
} },
}); });
const valueClass = computed(() => { const valueClass = computed(() => {
return _data.type === 'card' ? 'cardValue' : 'otherValue' return _data.type === 'card' ? 'cardValue' : 'otherValue';
}) });
const isHttps = document.location.protocol === 'https:'; const isHttps = document.location.protocol === 'https:';
const _types = ref<string>('') const _types = ref<string>('');
const visible = ref<boolean>(false) const visible = ref<boolean>(false);
const temp = ref<boolean>(false) const temp = ref<boolean>(false);
const onError = (e: any) => { const onError = (e: any) => {
e.target.src = imgMap.get('other') e.target.src = imgMap.get('other');
} };
const imgError = (e: any) => { const imgError = (e: any) => {
e.target.src = imgMap.get('error') e.target.src = imgMap.get('error');
temp.value = true temp.value = true;
} };
const getDetail = (_type: string) => { const getDetail = (_type: string) => {
const value = _data.value const value = _data.value;
let flag: string = '' let flag: string = '';
if(_type === 'img'){ if (_type === 'img') {
if (isHttps && value?.formatValue.indexOf('http:') !== -1) { if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
message.error('域名为https时不支持访问http地址'); message.error('域名为https时不支持访问http地址');
} else if (temp.value) { } else if (temp.value) {
message.error('该图片无法访问'); message.error('该图片无法访问');
} else { } else {
flag = ['.jpg', '.png'].find((item) => value?.formatValue.includes(item)) || '--'; flag =
['.jpg', '.png'].find((item) =>
value?.formatValue.includes(item),
) || '--';
_types.value = flag;
visible.value = true;
}
} else if (_type === 'video') {
if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
message.error('域名为https时不支持访问http地址');
} else if (
['.rmvb', '.mvb'].some((item) => value?.formatValue.includes(item))
) {
message.error('当前仅支持播放.mp4,.flv,.m3u8格式的视频');
} else {
flag =
['.m3u8', '.flv', '.mp4'].find((item) =>
value?.formatValue.includes(item),
) || '--';
_types.value = flag;
visible.value = true;
}
} else if (_type === 'obj') {
flag = 'obj';
_types.value = flag;
visible.value = true;
} }
} else if(_type === 'video'){ };
if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
message.error('域名为https时不支持访问http地址');
} else if (['.rmvb', '.mvb'].some((item) => value?.formatValue.includes(item))) {
message.error('当前仅支持播放.mp4,.flv,.m3u8格式的视频');
} else {
flag = ['.m3u8', '.flv', '.mp4'].find((item) => value?.formatValue.includes(item)) || '--';
}
}else if(_type === 'obj'){
flag = 'obj'
}
_types.value = flag
visible.value = true
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.value { .value {
display: flex;
align-items: center;
width: 100%;
.cardValue {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 60px;
overflow: hidden;
color: #323130;
font-weight: 700;
font-size: 24px;
white-space: nowrap;
text-overflow: ellipsis;
img { .cardValue {
width: 60px; display: flex;
} align-items: center;
} width: 100%;
height: 60px;
overflow: hidden;
color: #323130;
font-weight: 700;
font-size: 24px;
white-space: nowrap;
text-overflow: ellipsis;
.otherValue { img {
img { width: 60px;
width: 40px; }
}
.otherValue {
img {
width: 40px;
}
} }
}
} }
</style> </style>

View File

@ -2,7 +2,8 @@
<j-spin :spinning="loading"> <j-spin :spinning="loading">
<JProTable <JProTable
:columns="columns" :columns="columns"
:dataSource="dataSource" :request="query"
:params="_params"
:bodyStyle="{ padding: '0 0 0 20px' }" :bodyStyle="{ padding: '0 0 0 20px' }"
> >
<template #headerTitle> <template #headerTitle>
@ -58,26 +59,6 @@
</template> </template>
</j-space> </j-space>
</template> </template>
<template #paginationRender>
<j-pagination
size="small"
:total="total"
:showQuickJumper="false"
:showSizeChanger="true"
:current="pageIndex + 1"
:pageSize="pageSize"
:pageSizeOptions="['8', '12', '24', '60', '100']"
:show-total="
(num) =>
`${pageIndex * pageSize + 1} - ${
(pageIndex + 1) * pageSize > num
? num
: (pageIndex + 1) * pageSize
} /总共 ${num} `
"
@change="pageChange"
/>
</template>
</JProTable> </JProTable>
</j-spin> </j-spin>
<Save v-if="editVisible" @close="editVisible = false" :data="currentInfo" /> <Save v-if="editVisible" @close="editVisible = false" :data="currentInfo" />
@ -107,6 +88,7 @@ import { message } from 'ant-design-vue';
import { getWebSocket } from '@/utils/websocket'; import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { queryDashboard } from '@/api/comm'; import { queryDashboard } from '@/api/comm';
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@ -142,9 +124,6 @@ const _data = defineProps({
const value = ref<string>(''); const value = ref<string>('');
const dataSource = ref<PropertyData[]>([]); const dataSource = ref<PropertyData[]>([]);
const _dataSource = ref<PropertyData[]>([]); const _dataSource = ref<PropertyData[]>([]);
const pageIndex = ref<number>(0);
const pageSize = ref<number>(8);
const total = ref<number>(0);
const editVisible = ref<boolean>(false); // const editVisible = ref<boolean>(false); //
const detailVisible = ref<boolean>(false); // const detailVisible = ref<boolean>(false); //
const currentInfo = ref<Record<string, any>>({}); const currentInfo = ref<Record<string, any>>({});
@ -152,6 +131,9 @@ const instanceStore = useInstanceStore();
const indicatorVisible = ref<boolean>(false); // const indicatorVisible = ref<boolean>(false); //
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const propertyValue = ref<Record<string, any>>({}); const propertyValue = ref<Record<string, any>>({});
const _params = reactive({
name: '',
});
const subRef = ref(); const subRef = ref();
@ -309,39 +291,35 @@ const getDashboard = async () => {
loading.value = false; loading.value = false;
}; };
const query = (page: number, size: number, value: string) => { const query = (params: Record<string, any>) =>
pageIndex.value = page || 0; new Promise((resolve) => {
pageSize.value = size || 8; const _from = params.pageIndex * params.pageSize;
const _from = pageIndex.value * pageSize.value; const _to = (params.pageIndex + 1) * params.pageSize;
const _to = (pageIndex.value + 1) * pageSize.value; let arr = _.cloneDeep(_dataSource.value);
const arr = _.cloneDeep(_dataSource.value); if (params?.name) {
if (unref(value)) { const li = _dataSource.value.filter((i: any) => {
const li = arr.filter((i: any) => { return i?.name.indexOf(params.name) !== -1;
return i?.name.indexOf(unref(value)) !== -1; });
arr = _.cloneDeep(li);
}
resolve({
result: {
data: arr.slice(_from, _to),
pageIndex: params.pageIndex || 0,
pageSize: params.pageSize || 12,
total: arr.length,
},
status: 200,
}); });
dataSource.value = li.slice(_from, _to); getDashboard();
total.value = li.length; });
} else {
dataSource.value = arr.slice(_from, _to);
total.value = arr.length;
}
getDashboard();
};
const pageChange = (page: number, size: number) => {
if (size === pageSize.value) {
query(page - 1, size, value.value);
} else {
query(0, size, value.value);
}
};
watch( watch(
() => _data.data, () => _data.data,
(newVal) => { (newVal) => {
if (newVal.length) { if (newVal.length) {
_dataSource.value = newVal as PropertyData[]; _dataSource.value = newVal as PropertyData[];
query(0, 8, value.value); _params.name = '';
} }
}, },
{ {
@ -351,7 +329,7 @@ watch(
); );
const onSearch = () => { const onSearch = () => {
query(0, 8, value.value); _params.name = value.value;
}; };
onUnmounted(() => { onUnmounted(() => {

View File

@ -0,0 +1,175 @@
<!-- 绑定设备 -->
<template>
<j-modal
:maskClosable="false"
width="1000px"
:visible="true"
title="绑定父设备"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"
>
<j-advanced-search
:columns="columns"
target="child-device-bind"
@search="handleSearch"
type="simple"
/>
<JProTable
ref="bindDeviceRef"
:columns="columns"
:request="query"
model="TABLE"
:bodyStyle="{ padding: '0 0 0 24px' }"
:defaultParams="defaultParams"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
:params="params"
>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? dayjs(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
</JProTable>
</j-modal>
</template>
<script setup lang="ts">
import { query, bindDevice } from '@/api/device/instance';
import dayjs from 'dayjs';
import { message } from 'jetlinks-ui-components';
const emit = defineEmits(['cancel', 'ok']);
const bindDeviceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const btnLoading = ref<boolean>(false);
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const defaultParams = {
terms: [
{
column: 'productId$product-info',
value: [
{
column: 'deviceType',
termType: 'eq',
value: 'gateway',
},
],
},
],
};
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '所属产品',
dataIndex: 'productName',
key: 'productName',
search: {
type: 'string',
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
];
const handleSearch = (e: any) => {
params.value = e;
};
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const handleOk = () => {
if (_selectedRowKeys.value.length === 0) {
message.warning('请选择需要绑定的设备');
return;
}
btnLoading.value = true;
bindDevice(_selectedRowKeys.value[0], props.data.id, )
.then((resp) => {
if(resp.status === 200){
emit('ok', _selectedRowKeys.value[0]);
message.success('操作成功');
}
})
.finally(() => {
btnLoading.value = false;
});
};
const handleCancel = () => {
emit('cancel', false);
};
</script>
<style scoped lang="less"></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
width="900px" width="900px"
title="批量映射" title="批量映射"
visible visible
@ -10,49 +10,49 @@
<div class="map-tree-top"> <div class="map-tree-top">
采集器的点位名称与属性名称一致时将自动映射绑定有多个采集器点位名称与属性名称一致时以第1个采集器的点位数据进行绑定 采集器的点位名称与属性名称一致时将自动映射绑定有多个采集器点位名称与属性名称一致时以第1个采集器的点位数据进行绑定
</div> </div>
<a-spin :spinning="loading"> <j-spin :spinning="loading">
<div class="map-tree-content"> <div class="map-tree-content">
<a-card class="map-tree-content-card" title="源数据"> <j-card class="map-tree-content-card" title="源数据">
<a-tree <j-tree
checkable checkable
:height="300" :height="300"
:tree-data="dataSource" :tree-data="dataSource"
:checkedKeys="checkedKeys" :checkedKeys="checkedKeys"
@check="onCheck" @check="onCheck"
/> />
</a-card> </j-card>
<div style="width: 100px"> <div style="width: 100px">
<a-button <j-button
:disabled="rightList.length >= leftList.length" :disabled="rightList.length >= leftList.length"
@click="onRight" @click="onRight"
>加入右侧</a-button >加入右侧</j-button
> >
</div> </div>
<a-card class="map-tree-content-card" title="采集器"> <j-card class="map-tree-content-card" title="采集器">
<a-list <j-list
size="small" size="small"
:data-source="rightList" :data-source="rightList"
class="map-tree-content-card-list" class="map-tree-content-card-list"
> >
<template #renderItem="{ item }"> <template #renderItem="{ item }">
<a-list-item> <j-list-item>
{{ item.title }} {{ item.title }}
<template #actions> <template #actions>
<a-popconfirm <j-popconfirm
title="确定删除?" title="确定删除?"
@confirm="_delete(item.key)" @confirm="_delete(item.key)"
> >
<AIcon type="DeleteOutlined" /> <AIcon type="DeleteOutlined" />
</a-popconfirm> </j-popconfirm>
</template> </template>
</a-list-item> </j-list-item>
</template> </template>
</a-list> </j-list>
</a-card> </j-card>
</div> </div>
</a-spin> </j-spin>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,46 +1,46 @@
<template> <template>
<a-spin :spinning="loading" v-if="metadata.properties.length"> <j-spin :spinning="loading" v-if="metadata.properties.length">
<a-card> <j-card>
<template #extra> <template #extra>
<a-space> <j-space>
<a-button @click="visible = true">批量映射</a-button> <j-button @click="visible = true">批量映射</j-button>
<a-button type="primary" @click="onSave">保存</a-button> <j-button type="primary" @click="onSave">保存</j-button>
</a-space> </j-space>
</template> </template>
<a-form ref="formRef" :model="modelRef"> <j-form ref="formRef" :model="modelRef">
<a-table :dataSource="modelRef.dataSource" :columns="columns"> <j-table :dataSource="modelRef.dataSource" :columns="columns">
<template #headerCell="{ column }"> <template #headerCell="{ column }">
<template v-if="column.key === 'collectorId'"> <template v-if="column.key === 'collectorId'">
采集器 采集器
<a-tooltip title="数据采集中配置的真实物理设备"> <j-tooltip title="数据采集中配置的真实物理设备">
<AIcon type="QuestionCircleOutlined" /> <AIcon type="QuestionCircleOutlined" />
</a-tooltip> </j-tooltip>
</template> </template>
</template> </template>
<template #bodyCell="{ column, record, index }"> <template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'channelId'"> <template v-if="column.dataIndex === 'channelId'">
<a-form-item <j-form-item
:name="['dataSource', index, 'channelId']" :name="['dataSource', index, 'channelId']"
> >
<a-select <j-select
style="width: 100%" style="width: 100%"
v-model:value="record[column.dataIndex]" v-model:value="record[column.dataIndex]"
placeholder="请选择" placeholder="请选择"
allowClear allowClear
:filter-option="filterOption" :filter-option="filterOption"
> >
<a-select-option <j-select-option
v-for="item in channelList" v-for="item in channelList"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
:label="item.label" :label="item.label"
>{{ item.label }}</a-select-option >{{ item.label }}</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
</template> </template>
<template v-if="column.dataIndex === 'collectorId'"> <template v-if="column.dataIndex === 'collectorId'">
<a-form-item <j-form-item
:name="['dataSource', index, 'collectorId']" :name="['dataSource', index, 'collectorId']"
:rules="[ :rules="[
{ {
@ -54,10 +54,10 @@
:id="record.channelId" :id="record.channelId"
type="COLLECTOR" type="COLLECTOR"
/> />
</a-form-item> </j-form-item>
</template> </template>
<template v-if="column.dataIndex === 'pointId'"> <template v-if="column.dataIndex === 'pointId'">
<a-form-item <j-form-item
:name="['dataSource', index, 'pointId']" :name="['dataSource', index, 'pointId']"
:rules="[ :rules="[
{ {
@ -71,33 +71,33 @@
:id="record.collectorId" :id="record.collectorId"
type="POINT" type="POINT"
/> />
</a-form-item> </j-form-item>
</template> </template>
<template v-if="column.dataIndex === 'id'"> <template v-if="column.dataIndex === 'id'">
<a-badge <j-badge
v-if="record[column.dataIndex]" v-if="record[column.dataIndex]"
status="success" status="success"
text="已绑定" text="已绑定"
/> />
<a-badge v-else status="error" text="未绑定" /> <j-badge v-else status="error" text="未绑定" />
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a-tooltip title="解绑"> <j-tooltip title="解绑">
<a-popconfirm <j-popconfirm
title="确认解绑" title="确认解绑"
:disabled="!record.id" :disabled="!record.id"
@confirm="unbind(record.id)" @confirm="unbind(record.id)"
> >
<a-button type="link" :disabled="!record.id" <j-button type="link" :disabled="!record.id"
><AIcon type="icon-jiebang" ><AIcon type="icon-jiebang"
/></a-button> /></j-button>
</a-popconfirm> </j-popconfirm>
</a-tooltip> </j-tooltip>
</template> </template>
</template> </template>
</a-table> </j-table>
</a-form> </j-form>
</a-card> </j-card>
<PatchMapping <PatchMapping
:deviceId="instanceStore.current.id" :deviceId="instanceStore.current.id"
v-if="visible" v-if="visible"
@ -106,10 +106,10 @@
:type="provider" :type="provider"
:metaData="modelRef.dataSource" :metaData="modelRef.dataSource"
/> />
</a-spin> </j-spin>
<a-card v-else> <j-card v-else>
<JEmpty description='暂无数据,请配置物模型' style="margin: 10% 0" /> <JEmpty description='暂无数据,请配置物模型' style="margin: 10% 0" />
</a-card> </j-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,14 +1,14 @@
<template> <template>
<a-select allowClear v-model:value="_value" @change="onChange" placeholder="请选择" style="width: 100%"> <j-select allowClear v-model:value="_value" @change="onChange" placeholder="请选择" style="width: 100%">
<a-select-option <j-select-option
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
:value="item.id" :value="item.id"
:label="item.name" :label="item.name"
:filter-option="filterOption" :filter-option="filterOption"
>{{ item.name }}</a-select-option >{{ item.name }}</j-select-option
> >
</a-select> </j-select>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -10,9 +10,9 @@
<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>
<a-divider type="vertical" /> <j-divider type="vertical" />
<a-space> <j-space>
<a-badge <j-badge
:text="instanceStore.current.state?.text" :text="instanceStore.current.state?.text"
:status=" :status="
statusMap.get( statusMap.get(
@ -49,7 +49,7 @@
> >
断开连接 断开连接
</PermissionButton> </PermissionButton>
<a-tooltip <j-tooltip
v-if=" v-if="
instanceStore.current?.accessProvider === instanceStore.current?.accessProvider ===
'child-device' && 'child-device' &&
@ -68,15 +68,15 @@
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
style="font-size: 14px" style="font-size: 14px"
/> />
</a-tooltip> </j-tooltip>
</a-space> </j-space>
</div> </div>
<div style="padding-top: 10px"> <div style="padding-top: 10px">
<a-descriptions size="small" :column="4"> <j-descriptions size="small" :column="4">
<a-descriptions-item label="ID">{{ <j-descriptions-item label="ID">{{
instanceStore.current.id instanceStore.current.id
}}</a-descriptions-item> }}</j-descriptions-item>
<a-descriptions-item label="所属产品"> <j-descriptions-item label="所属产品">
<PermissionButton <PermissionButton
type="link" type="link"
style="margin-top: -5px; padding: 0" style="margin-top: -5px; padding: 0"
@ -85,8 +85,8 @@
> >
{{ instanceStore.current.productName }} {{ instanceStore.current.productName }}
</PermissionButton> </PermissionButton>
</a-descriptions-item> </j-descriptions-item>
</a-descriptions> </j-descriptions>
</div> </div>
</div> </div>
</template> </template>
@ -119,7 +119,7 @@ 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 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { getWebSocket } from '@/utils/websocket'; import { getWebSocket } from '@/utils/websocket';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';

View File

@ -1,5 +1,12 @@
<template> <template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="导出" @ok="handleOk" @cancel="handleCancel"> <j-modal
:maskClosable="false"
width="800px"
:visible="true"
title="导出"
@ok="handleOk"
@cancel="handleCancel"
>
<div style="background-color: rgb(236, 237, 238)"> <div style="background-color: rgb(236, 237, 238)">
<p style="padding: 10px"> <p style="padding: 10px">
<AIcon type="ExclamationCircleOutlined" /> <AIcon type="ExclamationCircleOutlined" />
@ -7,63 +14,86 @@
</p> </p>
</div> </div>
<div style="margin-top: 20px"> <div style="margin-top: 20px">
<a-form :layout="'vertical'"> <j-form :layout="'vertical'">
<a-form-item label="产品"> <j-form-item label="产品">
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品"> <j-select
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option> show-search
</a-select> :filter-option="filterOption"
</a-form-item> v-model:value="modelRef.product"
<a-form-item label="文件格式"> placeholder="请选择产品"
<a-radio-group button-style="solid" v-model:value="modelRef.fileType" placeholder="请选择文件格式"> allowClear
<a-radio-button value="xlsx">xlsx</a-radio-button> >
<a-radio-button value="csv">csv</a-radio-button> <j-select-option
</a-radio-group> :value="item.id"
</a-form-item> v-for="item in productList"
</a-form> :key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
<j-form-item label="文件格式">
<j-radio-group
button-style="solid"
v-model:value="modelRef.fileType"
placeholder="请选择文件格式"
>
<j-radio-button value="xlsx">xlsx</j-radio-button>
<j-radio-button value="csv">csv</j-radio-button>
</j-radio-group>
</j-form-item>
</j-form>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
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 { 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']);
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: undefined default: undefined,
} },
}) });
const modelRef = reactive({ const modelRef = reactive({
product: undefined, product: undefined,
fileType: 'xlsx' fileType: 'xlsx',
}); });
const productList = ref<Record<string, any>[]>([]) const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const productList = ref<Record<string, any>[]>([]);
watch( watch(
() => props.data, () => props.data,
() => { () => {
queryNoPagingPost({paging: false}).then(resp => { queryNoPagingPost({ paging: false }).then((resp) => {
if(resp.status === 200){ if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[] productList.value = resp.result as Record<string, any>[];
} }
}) });
}, },
{immediate: true, deep: true} { immediate: true, deep: true },
) );
const handleOk = () => { const handleOk = () => {
const params = encodeQuery(props.data); const params = encodeQuery(props.data);
downloadFile(deviceExport(modelRef.product || "", modelRef.fileType),params); downloadFile(
emit('close') deviceExport(modelRef.product || '', modelRef.fileType),
} params,
);
emit('close');
};
const handleCancel = () => { const handleCancel = () => {
emit('close') emit('close');
} };
</script> </script>

View File

@ -1,36 +1,35 @@
<template> <template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleCancel" @cancel="handleCancel"> <j-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleSave" @cancel="handleCancel">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<a-form :layout="'vertical'"> <j-form :layout="'vertical'">
<a-row> <j-row>
<a-col span="24"> <j-col span="24">
<a-form-item label="产品" required> <j-form-item label="产品" required>
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品"> <j-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option> <j-select-option :value="item.id" v-for="item in productList" :key="item.id" :label="item.name">{{ 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 label="文件格式" v-if="modelRef.product"> <j-form-item label="文件格式" v-if="modelRef.product">
<FileFormat v-model="modelRef.file" /> <FileFormat v-model="modelRef.file" />
</a-form-item> </j-form-item>
</a-col> </j-col>
<a-col span="12"> <j-col span="12">
<a-form-item label="文件上传" v-if="modelRef.product"> <j-form-item label="文件上传" v-if="modelRef.product">
<NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" /> <NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" />
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
</a-form> </j-form>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product' import { queryNoPagingPost } from '@/api/device/product'
import { Form } from 'ant-design-vue';
const emit = defineEmits(['close']) const emit = defineEmits(['close', 'save'])
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
@ -38,7 +37,6 @@ const props = defineProps({
} }
}) })
const productList = ref<Record<string, any>[]>([]) const productList = ref<Record<string, any>[]>([])
const useForm = Form.useForm;
const modelRef = reactive({ const modelRef = reactive({
product: undefined, product: undefined,
@ -64,4 +62,8 @@ watch(
const handleCancel = () => { const handleCancel = () => {
emit('close') emit('close')
} }
const handleSave = () => {
emit('save')
}
</script> </script>

View File

@ -1,12 +1,12 @@
<template> <template>
<a-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>
<a-badge v-if="flag" status="processing" text="进行中" /> <j-badge v-if="flag" status="processing" text="进行中" />
<a-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>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -18,7 +18,7 @@
<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">
<JUpload v-model="modelRef.photoUrl" /> <JProUpload v-model="modelRef.photoUrl" />
</j-form-item> </j-form-item>
</j-col> </j-col>
<j-col flex="auto"> <j-col flex="auto">
@ -60,15 +60,14 @@
<j-select <j-select
showSearch showSearch
v-model:value="modelRef.productId" v-model:value="modelRef.productId"
placeholder="请选择所属产品" :disabled="!!data?.id"
:filter-option="filterOption" placeholder="请选择状态为“正常”的产品"
> >
<j-select-option <j-select-option
:value="item.id" :value="item.id"
v-for="item in productList" v-for="item in productList"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:disabled="!!data?.id"
>{{item.name}}</j-select-option> >{{item.name}}</j-select-option>
</j-select> </j-select>
</j-form-item> </j-form-item>
@ -89,7 +88,7 @@
import { queryNoPagingPost } from '@/api/device/product'; import { queryNoPagingPost } from '@/api/device/product';
import { isExists, update } from '@/api/device/instance'; import { isExists, update } from '@/api/device/instance';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
const emit = defineEmits(['close', 'save']); const emit = defineEmits(['close', 'save']);
const props = defineProps({ const props = defineProps({
@ -105,16 +104,12 @@ const formRef = ref();
const modelRef = reactive({ const modelRef = reactive({
productId: undefined, productId: undefined,
id: '', id: undefined,
name: '', name: '',
describe: '', describe: '',
photoUrl: getImage('/device/instance/device-card.png'), photoUrl: getImage('/device/instance/device-card.png'),
}); });
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const vailId = async (_: Record<string, any>, value: string) => { const vailId = async (_: Record<string, any>, value: string) => {
if (!props?.data?.id && value) { if (!props?.data?.id && value) {
const resp = await isExists(value); const resp = await isExists(value);
@ -202,10 +197,15 @@ const handleCancel = () => {
const handleSave = () => { const handleSave = () => {
formRef.value formRef.value
.validate() .validate()
.then(async () => { .then(async (_data: any) => {
loading.value = true; loading.value = true;
const resp = await update(toRaw(modelRef)); const obj = {...toRaw(modelRef), ..._data}
loading.value = false; if(!obj.id){
delete obj.id
}
const resp = await update(obj).finally(() => {
loading.value = false;
})
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
emit('save'); emit('save');

View File

@ -156,7 +156,10 @@
</template> </template>
<template #content> <template #content>
<Ellipsis style="width: calc(100% - 100px)"> <Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600" @click.stop="handleView(slotProps.id)"> <span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }} {{ slotProps.name }}
</span> </span>
</Ellipsis> </Ellipsis>
@ -205,6 +208,11 @@
:status="statusMap.get(slotProps.state?.value)" :status="statusMap.get(slotProps.state?.value)"
/> />
</template> </template>
<template #createTime="slotProps">
<span>{{
dayjs(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="slotProps"> <template #action="slotProps">
<j-space> <j-space>
<template <template
@ -229,11 +237,16 @@
</template> </template>
</JProTable> </JProTable>
</page-container> </page-container>
<Import v-if="importVisible" @close="importVisible = false" /> <Import
v-if="importVisible"
@close="importVisible = false"
@save="onRefresh"
/>
<Export <Export
v-if="exportVisible" v-if="exportVisible"
@close="exportVisible = false" @close="exportVisible = false"
:data="params" :data="params"
@save="onRefresh"
/> />
<Process <Process
v-if="operationVisible" v-if="operationVisible"
@ -259,9 +272,8 @@ import {
batchDeployDevice, batchDeployDevice,
batchDeleteDevice, batchDeleteDevice,
} from '@/api/device/instance'; } from '@/api/device/instance';
// import type { ActionsType } from '@/components/Table/index.vue';
import { getImage, LocalStore } from '@/utils/comm'; import { getImage, LocalStore } from '@/utils/comm';
import { message } from 'ant-design-vue'; import { message } from 'jetlinks-ui-components';
import Import from './Import/index.vue'; import Import from './Import/index.vue';
import Export from './Export/index.vue'; import Export from './Export/index.vue';
import Process from './Process/index.vue'; import Process from './Process/index.vue';
@ -275,8 +287,9 @@ import {
} from '@/api/device/product'; } from '@/api/device/product';
import { queryTree } from '@/api/device/category'; import { queryTree } from '@/api/device/category';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';
import type { ActionsType } from './typings';
import dayjs from 'dayjs';
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 _selectedRowKeys = ref<string[]>([]); const _selectedRowKeys = ref<string[]>([]);
@ -696,4 +709,8 @@ const saveBtn = () => {
const handleSearch = (_params: any) => { const handleSearch = (_params: any) => {
params.value = _params; params.value = _params;
}; };
const onRefresh = () => {
instanceRef.value?.reload();
};
</script> </script>

View File

@ -83,4 +83,17 @@ type InstanceModel = {
params: Set<string>; // 处理无限循环Card params: Set<string>; // 处理无限循环Card
active?: string; // 当前编辑的Card active?: string; // 当前编辑的Card
selectedRows: Map<string, any>; selectedRows: Map<string, any>;
}
export interface ActionsType {
key: string;
text?: string;
disabled?: boolean;
permission?: boolean;
onClick?: (data: any) => void;
style?: CSSProperties;
tooltip?: TooltipProps;
popConfirm?: PopconfirmProps;
icon?: string;
children?: ActionsType[];
} }

View File

@ -10,7 +10,7 @@
@cancel="_vis = false" @cancel="_vis = false"
:confirmLoading="loading" :confirmLoading="loading"
> >
<Search <j-advanced-search
type="simple" type="simple"
:columns="columns" :columns="columns"
target="media" target="media"

View File

@ -1,7 +1,7 @@
<!-- 国标级联-通道列表 --> <!-- 国标级联-通道列表 -->
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
type="simple" type="simple"
:columns="columns" :columns="columns"
target="media" target="media"

View File

@ -1,6 +1,6 @@
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="media-cascade" target="media-cascade"
@search="handleSearch" @search="handleSearch"

View File

@ -1,7 +1,7 @@
<!-- 视频设备-通道列表 --> <!-- 视频设备-通道列表 -->
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
type="simple" type="simple"
:columns="columns" :columns="columns"
target="product" target="product"

View File

@ -0,0 +1,79 @@
<!-- 视频图标组件 -->
<template>
<a @click="handleClick">
<AIcon :type="iconType" />
</a>
</template>
<script setup lang="ts">
import type { recordsItemType } from './typings';
import playBackApi from '@/api/media/playback';
import { message } from 'ant-design-vue';
interface Props {
type: string;
item: recordsItemType;
onCloudView: (startTime: number, endTime: number) => void;
onDownLoad: () => void;
}
const props = defineProps<Props>();
// type local0 12
const status = ref(props.item?.isServer ? 2 : 0);
// const status = computed({
// get: () => (props.item?.isServer ? 2 : 0),
// set: (val: number) => {},
// });
const getLocalIcon = (s: number) => {
if (s === 0) {
return 'CloudDownloadOutlined';
} else if (s === 2) {
return 'EyeOutlined';
} else {
return 'LoadingOutlined';
}
};
const iconType = computed(() =>
props.type === 'local' ? getLocalIcon(status.value) : 'DownloadOutlined',
);
const downLoadCloud = (item: recordsItemType) => {
status.value = 1;
playBackApi
.downloadRecord(item.deviceId, item.channelId, {
local: false,
downloadSpeed: 4,
startTime: item.startTime,
endTime: item.endTime,
})
.then((res) => {
if (res.status === 200) {
message.success(
'操作成功。上传云端需要一定时间,请稍后查看云端数据',
);
}
status.value = res.status === 200 ? 2 : 0;
})
.catch(() => {
status.value = 0;
});
};
const handleClick = () => {
if (props.type === 'local') {
if (status.value === 2) {
//
props.onCloudView(props.item.startTime, props.item.endTime);
} else if (status.value === 0) {
//
downLoadCloud(props.item);
}
} else {
props.onDownLoad();
}
};
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,119 @@
@borderColor: #d9d9d9;
.playback-warp {
display: flex;
padding: 24px;
background-color: #fff;
.playback-left {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 0;
.playback-media {
width: 100%;
}
}
.playback-right {
width: 300px;
margin-left: 24px;
.playback-calendar {
margin-top: 16px;
border: 1px solid @borderColor;
border-radius: 2px;
:deep(.ant-picker-calendar-header) {
justify-content: space-between;
& > div:nth-child(3) {
display: none !important;
}
}
}
.playback-list {
display: flex;
height: 300px;
margin-top: 16px;
overflow-y: auto;
border: 1px solid @borderColor;
&.no-list {
align-items: center;
justify-content: center;
}
.playback-list-items {
width: 100%;
.ant-list-item {
padding-left: 12px;
}
}
}
}
.time-line-warp {
padding: 10px 0;
.time-line-clock {
display: flex;
align-items: stretch;
justify-content: space-between;
width: 100%;
> div {
color: #666;
font-size: 12px;
}
}
.time-line-content {
position: relative;
padding-bottom: 20px;
.time-line-progress {
position: relative;
height: 16px;
overflow: hidden;
background-color: #d9d9d9;
border-radius: 2px;
> div {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #52c41a;
cursor: pointer;
}
}
.time-line-btn {
position: absolute;
top: -2px;
left: 0;
width: 3px;
height: 19px;
background-color: @primary-color;
border-radius: 2px;
visibility: hidden;
}
.time-line {
position: absolute;
bottom: -8px;
left: -30px;
width: 60px;
padding: 2px 0;
font-size: 12px;
text-align: center;
background-color: #d9d9d9;
border-radius: 2px;
box-shadow: 0 0 12px rgba(#000, 0.15);
visibility: hidden;
}
}
}
}

View File

@ -1,7 +1,366 @@
<!-- 回放 -->
<template> <template>
<page-container> 回放 </page-container> <page-container>
<div class="playback-warp">
<div class="playback-left">
<LivePlayer
ref="player"
:src="url"
width="758px"
height="462px"
/>
<TimeLine
ref="playTimeNode"
:type="type"
:data="historyList"
:date-time="time"
:on-change="handleTimeLineChange"
:play-status="playStatus"
:play-time="playNowTime + playTime * 1000"
:local-to-server="cloudTime"
/>
</div>
<div class="playback-right">
<a-spin :spinning="loading">
<a-tooltip placement="topLeft">
<template #title>
<div>云端存储在服务器中</div>
<div>本地存储在设备本地</div>
</template>
<div>类型: <AIcon type="QuestionCircleOutlined" /></div>
</a-tooltip>
<RadioCard
layout="horizontal"
:options="[
{
label: '云端',
value: 'cloud',
logo: getImage('/media/cloud.png'),
},
{
label: '本地',
value: 'local',
logo: getImage('/local.png'),
disabled: deviceType === 'fixed-media',
},
]"
:checkStyle="true"
v-model="type"
/>
<div class="playback-calendar">
<a-calendar
v-model:value="time"
:fullscreen="false"
:disabledDate="
(currentDate) => currentDate > dayjs(new Date())
"
@change="handlePanelChange"
/>
</div>
<div
class="playback-list"
:class="{ 'no-list': !historyList.length }"
>
<a-empty
v-if="!historyList.length"
description="暂无数据"
/>
<a-list
v-else
class="playback-list-items"
itemLayout="horizontal"
:dataSource="historyList"
>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-tooltip
key="play-btn"
:title="
(item.startTime ||
item.mediaStartTime) ===
playNowTime &&
playStatus === 1
? '暂停'
: '播放'
"
>
<a
@click="
handlePlay(
item.startTime ||
item.mediaStartTime,
)
"
>
<AIcon
:type="
(item.startTime ||
item.mediaStartTime) ===
playNowTime &&
playStatus === 1
? 'PauseCircleOutlined'
: 'PlayCircleOutlined'
"
/>
</a>
</a-tooltip>
<a-tooltip
key="download"
:title="
type !== 'local'
? '下载录像文件'
: item.isServer
? '查看'
: '下载到云端'
"
>
<IconNode
:type="type"
:item="item"
:on-cloud-view="cloudView"
:on-down-load="
() => downloadClick(item)
"
/>
</a-tooltip>
</template>
<div>
{{
dayjs(
item.startTime ||
item.mediaStartTime,
).format('HH:mm:ss')
}}
~
{{
dayjs(
item.endTime ||
item.mediaEndTime,
).format('HH:mm:ss')
}}
</div>
</a-list-item>
</template>
<div></div>
</a-list>
</div>
</a-spin>
</div>
</div>
</page-container>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import playBackApi from '@/api/media/playback';
import TimeLine from './timeLine.vue';
import IconNode from './iconNode.vue';
import type { recordsItemType } from './typings';
import LivePlayer from '@/components/Player/index.vue';
import { getImage } from '@/utils/comm';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
<style lang="less" scoped></style> const route = useRoute();
const url = ref('');
const type = ref('local');
const historyList = ref<recordsItemType[]>([]);
const time = ref<Dayjs | undefined>(undefined);
const loading = ref(false);
const cloudTime = ref<any>();
// const location = ref();
const player = ref<any>();
const playStatus = ref(0); // , 0 1 2 , 3
const playTime = ref(0);
const playNowTime = ref(0); //
const playTimeNode = ref<any>(null);
const isEnded = ref(false); //
const deviceId = computed(() => route.query.id as string);
const channelId = computed(() => route.query.channelId as string);
const deviceType = ref('');
const queryLocalRecords = async (date: Dayjs) => {
console.log('date: ', date);
playStatus.value = 0;
url.value = '';
if (deviceId.value && channelId.value && date) {
loading.value = true;
const params = {
startTime: date.format('YYYY-MM-DD 00:00:00'),
endTime: date.format('YYYY-MM-DD 23:59:59'),
};
const localResp = await playBackApi.queryRecordLocal(
deviceId.value,
channelId.value,
params,
);
if (localResp.status === 200 && localResp.result.length) {
const serviceResp = await playBackApi.recordsInServer(
deviceId.value,
channelId.value,
{
...params,
includeFiles: false,
},
);
loading.value = false;
let newList: recordsItemType[] = serviceResp.result;
// console.log(newList)
if (serviceResp.status === 200 && serviceResp.result) {
//
newList = localResp.result.map((item: recordsItemType) => {
return {
...item,
isServer: serviceResp.result.length
? serviceResp.result.some(
(serverFile: any) =>
item.startTime <=
serverFile.streamStartTime &&
serverFile.streamEndTime <= item.endTime,
)
: false,
};
});
historyList.value = newList;
} else {
historyList.value = newList;
}
} else {
loading.value = false;
historyList.value = [];
}
}
};
/**
* 查询云端视频
* @param date
*/
const queryServiceRecords = async (date: Dayjs) => {
playStatus.value = 0;
url.value = '';
if (deviceId.value && channelId.value && date) {
loading.value = true;
const params = {
startTime: date.format('YYYY-MM-DD 00:00:00'),
endTime: date.format('YYYY-MM-DD 23:59:59'),
includeFiles: true,
};
const resp = await playBackApi.recordsInServerFiles(
deviceId.value,
channelId.value,
params,
);
loading.value = false;
if (resp.status === 200) {
historyList.value = resp.result;
}
}
};
const cloudView = (startTime: number, endTime: number) => {
type.value = 'cloud';
cloudTime.value = { startTime, endTime };
queryServiceRecords(time.value!);
};
const downloadClick = async (item: recordsItemType) => {
const downloadUrl = playBackApi.downLoadFile(item.id);
const downNode = document.createElement('a');
downNode.href = downloadUrl;
downNode.download = `${channelId}-${dayjs(item.startTime).format(
'YYYY-MM-DD-HH-mm-ss',
)}.mp4`;
downNode.style.display = 'none';
document.body.appendChild(downNode);
downNode.click();
document.body.removeChild(downNode);
};
onMounted(() => {
const _type = route.query.type as string;
if (_type) {
deviceType.value = _type;
const _timeStr = dayjs(new Date());
time.value = _timeStr;
if (_type === 'fixed-media') {
type.value = 'cloud';
queryServiceRecords(_timeStr);
} else {
queryLocalRecords(_timeStr);
type.value = 'local';
}
}
});
const handleTimeLineChange = (times: any) => {
if (times) {
playNowTime.value = Number(times.startTime.valueOf());
playTime.value = 0;
url.value =
type.value === 'local'
? playBackApi.playbackLocal(
times.deviceId,
times.channelId,
'mp4',
dayjs(times.startTime).format('YYYY-MM-DD HH:mm:ss'),
dayjs(times.endTime).format('YYYY-MM-DD HH:mm:ss'),
)
: playBackApi.playbackStart(times.deviceId);
} else {
url.value = '';
}
};
watch(
() => type.value,
(val: string) => {
if (val === 'cloud') {
queryServiceRecords(time.value!);
} else {
queryLocalRecords(time.value!);
}
},
);
const handlePanelChange = (date: any) => {
console.log('type: ', typeof date);
console.log('date: ', date);
time.value = date;
if (type.value === 'cloud') {
queryServiceRecords(date);
} else {
queryLocalRecords(date);
}
};
// /
const handlePlay = (_startTime: any) => {
if (playStatus.value === 0 || _startTime !== playNowTime.value) {
if (playTimeNode.value) {
playTimeNode.value.playByStartTime(_startTime);
}
} else if (playStatus.value == 1 && _startTime === playNowTime.value) {
if (player.value.getVueInstance) {
// player.value.getVueInstance().pause();
}
} else if (playStatus.value == 2 && _startTime === playNowTime.value) {
if (player.value.getVueInstance) {
// player.value.getVueInstance().play();
}
}
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,264 @@
<!-- 播放器时间刻度 -->
<template>
<div class="time-line-warp">
<div class="time-line-clock">
<div
v-for="item in Array.from(Array(25), (v, k) => k)"
:key="item"
style="width: 12px"
>
{{ item }}
</div>
</div>
<div class="time-line-content" ref="LineContent">
<div class="time-line-progress">
<div
v-for="(item, index) in list"
:key="`time_${index}`"
@click="handleProgress($event, item)"
:style="
getLineItemStyle(
item.startTime || item.mediaStartTime,
item.endTime || item.mediaEndTime,
)
"
></div>
</div>
<div id="btn" class="time-line-btn"></div>
<div id="time" class="time-line">
{{ dayjs(playTime || 0).format('HH:mm:ss') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue';
import type { recordsItemType } from './typings';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
export type TimeChangeType = {
endTime: Dayjs;
startTime: Dayjs;
deviceId: string;
channelId: string;
};
interface Props {
onChange: (times: TimeChangeType | undefined) => void;
data: recordsItemType[];
dateTime?: Dayjs;
type: string;
playStatus: number;
playTime: number;
server?: any;
localToServer?: {
endTime: number;
startTime: number;
};
getPlayList?: (data: any) => void;
}
const props = defineProps<Props>();
//
const startT = ref<number>(
new Date(
dayjs(props.dateTime).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
).getTime(),
);
//
const endT = ref<number>(
new Date(
dayjs(props.dateTime).endOf('day').format('YYYY-MM-DD HH:mm:ss'),
).getTime(),
);
const list = ref<any[]>([]);
const playTime = ref<number>(0);
const LineContent = ref<HTMLDivElement>();
// const LineContentSize = LineContent.value;
const LineContentSize = ref({ width: 100 });
const setTimeAndPosition = (ob: number) => {
const oBtn = document.getElementById('btn');
const oTime = document.getElementById('time');
if (oBtn && oTime && LineContentSize.value.width) {
oBtn.style.visibility = 'visible';
oBtn.style.left = `${ob * LineContentSize.value.width}px`;
oTime.style.visibility = 'visible';
oTime.style.left = `${ob * LineContentSize.value.width - 15}px`;
}
};
watch(
() => props.dateTime,
(val: any) => {
startT.value = new Date(
dayjs(val).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
).getTime();
},
);
const onChange = (
startTime: number,
endTime: number,
deviceId: string,
channelId: string,
) => {
playTime.value = startTime;
props.onChange({
startTime: dayjs(startTime),
endTime: dayjs(endTime),
deviceId,
channelId,
});
};
const playByStartTime = (time: any) => {
const playNow = props.data.find((item) => {
const startTime = item.startTime || item.mediaStartTime;
return startTime === time;
});
if (playNow) {
const startTime = playNow.startTime || playNow.mediaStartTime;
const endTime = playNow.endTime || playNow.mediaEndTime;
const deviceId = props.type === 'local' ? playNow.deviceId : playNow.id;
onChange(startTime, endTime, deviceId, playNow.channelId);
}
};
playByStartTime(0);
const onNextPlay = () => {
if (playTime.value) {
//
const nowIndex = props.data.findIndex((item) => {
const startTime = item.startTime || item.mediaStartTime;
return startTime === playTime.value;
});
//
if (nowIndex !== props.data.length - 1) {
const nextPlay = props.data[nowIndex + 1];
const startTime = nextPlay.startTime || nextPlay.mediaStartTime;
const endTime = nextPlay.endTime || nextPlay.mediaEndTime;
const deviceId =
props.type === 'local' ? nextPlay.deviceId : nextPlay.id;
onChange(startTime, endTime, deviceId, nextPlay.channelId);
}
}
};
onNextPlay();
watch(
() => props.data,
(val: any) => {
const { data, localToServer, type } = props;
if (data && Array.isArray(data) && data.length > 0) {
list.value = [...data];
if (type === 'local') {
//
onChange(
data[0].startTime,
data[0].endTime,
data[0].deviceId,
data[0].channelId,
);
} else if (type === 'cloud') {
//
if (localToServer && Object.keys(localToServer).length > 0) {
//
const playItem = data.find((item) => {
return (
item.mediaEndTime <= localToServer.endTime &&
item.mediaStartTime >= localToServer.startTime
);
});
if (playItem) {
//
onChange(
playItem.mediaStartTime,
playItem.mediaEndTime,
playItem.id,
playItem.channelId,
);
} else {
props.onChange(undefined);
message.error('没有可播放的视频资源');
}
} else {
onChange(
data[0].mediaStartTime,
data[0].mediaEndTime,
data[0].id,
data[0].channelId,
);
}
}
} else if (localToServer && localToServer.startTime) {
//
props.onChange(undefined);
message.error('没有可播放的视频资源');
list.value = [];
} else {
//
list.value = [];
props.onChange(undefined);
}
},
);
const getLineItemStyle = (
startTime: number,
endTime: number,
): { left: string; width: string } => {
const start = startTime - startT.value > 0 ? startTime - startT.value : 0;
const _width = LineContentSize.value.width!;
const itemWidth = ((endTime - startTime) / (24 * 3600000)) * _width;
return {
left: `${(start / (24 * 3600000)) * _width}px`,
width: `${itemWidth < 1 ? 1 : itemWidth}px`,
};
};
const playTimeChange = () => {
if (
props.playTime &&
props.playTime >= startT.value &&
props.playTime <= endT.value &&
props.data &&
props.data.length
) {
setTimeAndPosition((props.playTime - startT.value) / 3600000 / 24);
}
};
watch(
() => props.playTime,
() => {
playTimeChange();
},
);
watch(
() => startT.value,
() => {
playTimeChange();
},
);
const handleProgress = (event: any, item: any) => {
const pos = LineContent.value?.getBoundingClientRect();
if (pos && item.endTime) {
const dt = event.clientX - pos.x;
const start = (dt / pos.width) * 24 * 3600000 + startT.value;
const _start = start < item.startTime ? item.startTime : start;
onChange(_start, item.endTime, item.deviceId, item.channelId);
}
};
defineExpose({ playByStartTime });
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="notice-config" target="notice-config"
@search="handleSearch" @search="handleSearch"

View File

@ -1,6 +1,6 @@
<template> <template>
<a-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%"> <a-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%">
<Search <j-search
type="simple" type="simple"
:columns="columns" :columns="columns"
target="product" target="product"

View File

@ -1,6 +1,6 @@
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="notice-config" target="notice-config"
@search="handleSearch" @search="handleSearch"

View File

@ -1,6 +1,6 @@
<template> <template>
<a-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%"> <a-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%">
<Search <j-search
type="simple" type="simple"
:columns="columns" :columns="columns"
target="product" target="product"

View File

@ -1,6 +1,6 @@
<template> <template>
<page-container> <page-container>
<Search <j-advanced-search
:columns="columns" :columns="columns"
target="notice-config" target="notice-config"
@search="handleSearch" @search="handleSearch"

View File

@ -1,6 +1,6 @@
<template> <template>
<page-container> <page-container>
<Search :columns="columns" target="scene" @search="handleSearch" /> <j-advanced-search :columns="columns" target="scene" @search="handleSearch" />
<JProTable <JProTable
ref="sceneRef" ref="sceneRef"
:columns="columns" :columns="columns"
@ -23,7 +23,6 @@
<template #card="slotProps"> <template #card="slotProps">
<SceneCard <SceneCard
:value="slotProps" :value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')" :actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value" :status="slotProps.state?.value"
:statusText="slotProps.state?.text" :statusText="slotProps.state?.text"
@ -50,7 +49,7 @@
<Ellipsis style="width: calc(100% - 100px)"> <Ellipsis style="width: calc(100% - 100px)">
<span <span
style="font-size: 16px; font-weight: 600" style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)" @click.stop="handleView(slotProps.id, slotProps.triggerType)"
> >
{{ slotProps.name }} {{ slotProps.name }}
</span> </span>
@ -126,7 +125,7 @@
import SaveModal from './Save/save.vue'; import SaveModal from './Save/save.vue';
import type { SceneItem } from './typings'; import type { SceneItem } from './typings';
import { useMenuStore } from 'store/menu'; import { useMenuStore } from 'store/menu';
import { query, _delete, _action } from '@/api/rule-engine/scene'; import { query, _delete, _action, _execute } from '@/api/rule-engine/scene';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { ActionsType } from '@/components/Table'; import type { ActionsType } from '@/components/Table';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
@ -180,10 +179,12 @@ const columns = [
scopedSlots: true, scopedSlots: true,
search: { search: {
type: 'select', type: 'select',
options: Array.from(typeMap).map((item) => ({ options: Array.from(typeMap).map((item) => {
label: item[1], return {
value: item[0], label: item[1]?.text,
})), value: item[0],
}
}),
}, },
}, },
{ {
@ -305,9 +306,18 @@ const getActions = (
: '未启用,不能手动触发', : '未启用,不能手动触发',
}, },
icon: 'LikeOutlined', icon: 'LikeOutlined',
onClick: () => { popConfirm: {
// handleView(data.id, data.triggerType); title: '',
}, onConfirm: async () => {
const resp = await _execute(data.id)
if (resp.status === 200) {
message.success('操作成功!');
sceneRef.value?.reload();
} else {
message.error('操作失败!');
}
}
}
}; };
actions.splice(1, 0, _item); actions.splice(1, 0, _item);
} }

View File

@ -1,16 +1,19 @@
<template> <template>
<div class="product-container"> <div class="product-container">
<Search :columns="query.columns" @search="query.search" /> <j-advanced-search
:columns="columns"
@search="(params:any) => (queryParams = params)"
/>
<j-pro-table <j-pro-table
ref="tableRef" ref="tableRef"
:request="table.requestFun" :request="table.requestFun"
:gridColumn="2" :gridColumn="2"
model="CARD" :params="queryParams"
:params="query.params.value"
:rowSelection="{ :rowSelection="{
selectedRowKeys: table._selectedRowKeys.value, selectedRowKeys: table._selectedRowKeys.value,
}" }"
@cancelSelect="table.cancelSelect" @cancelSelect="table.cancelSelect"
:columns="columns"
> >
<template #headerTitle> <template #headerTitle>
<j-space> <j-space>
@ -130,13 +133,46 @@
</template> </template>
</CardBox> </CardBox>
</template> </template>
<template #permission="slotProps">
{{
table.permissionList.value.length &&
table.getPermissLabel(slotProps.permission)
}}
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
type="link"
:tooltip="i?.tooltip"
:pop-confirm="i.popConfirm"
@click="i.onClick"
:disabled="i?.disabled"
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</template>
</j-pro-table> </j-pro-table>
<div class="dialogs"> <div class="dialogs">
<AddDeviceOrProductDialog <AddDeviceOrProductDialog
v-if="dialogs.addShow" v-if="dialogs.addShow"
v-model:visible="dialogs.addShow" v-model:visible="dialogs.addShow"
:query-columns="query.columns" :query-columns="columns"
:parent-id="props.parentId" :parent-id="props.parentId"
:all-permission="table.permissionList.value" :all-permission="table.permissionList.value"
asset-type="device" asset-type="device"
@ -171,7 +207,7 @@ import {
} from '@/api/system/department'; } from '@/api/system/department';
import { intersection } from 'lodash-es'; import { intersection } from 'lodash-es';
import type { dictType } from '../typing'; import type { dictType, optionsType } from '../typing';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
const permission = 'system/Department'; const permission = 'system/Department';
@ -181,93 +217,97 @@ const props = defineProps<{
parentId: string; parentId: string;
bindBool: boolean; bindBool: boolean;
}>(); }>();
const query = { const columns = [
columns: [ {
{ title: 'ID',
title: 'ID', dataIndex: 'id',
dataIndex: 'id', key: 'id',
key: 'id', ellipsis: true,
ellipsis: true, fixed: 'left',
fixed: 'left', search: {
search: { type: 'string',
type: 'string',
},
}, },
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '所属产品',
dataIndex: 'productId$product-info',
key: 'productId$product-info',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
const params = {
paging: false,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
};
getDeviceProduct_api(params).then((resp: any) => {
const result = resp.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
resolve(result);
});
}),
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
ellipsis: true,
fixed: 'left',
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: [
{
label: '在线',
value: 'online',
},
{
label: '离线',
value: 'offline',
},
{
label: '禁用',
value: 'notActive',
},
],
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
}, },
}; {
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '所属产品',
dataIndex: 'productName',
key: 'productName',
ellipsis: true,
search: {
rename: 'productId$product-info',
type: 'select',
options: () =>
new Promise((resolve) => {
const params = {
paging: false,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
};
getDeviceProduct_api(params).then((resp: any) => {
const result = resp.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
resolve(result);
});
}),
},
},
{
title: '资产权限',
dataIndex: 'permission',
key: 'permission',
ellipsis: true,
scopedSlots: true,
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
ellipsis: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
scopedSlots: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
fixed: 'right',
scopedSlots: true,
},
];
const queryParams = ref({});
const tableRef = ref(); const tableRef = ref();
const table = { const table = {
@ -285,6 +325,32 @@ const table = {
); );
}, },
getActions: (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
) => {
if (!data) return [];
else
return [
{
permission: true,
key: 'edit',
tooltip: { title: '编辑' },
icon: 'EditOutlined',
onClick: () => table.clickEdit(data),
},
{
permission: true,
key: 'unbind',
tooltip: { title: '解除绑定' },
popConfirm: {
title: `是否解除绑定`,
onConfirm: () => table.clickUnBind(data),
},
icon: 'DisconnectOutlined',
},
];
},
// //
getPermissionDict: () => { getPermissionDict: () => {
getPermissionDict_api().then((resp: any) => { getPermissionDict_api().then((resp: any) => {

View File

@ -1,15 +1,18 @@
<template> <template>
<div class="product-container"> <div class="product-container">
<Search :columns="query.columns" @search="query.search" /> <j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table <j-pro-table
ref="tableRef" ref="tableRef"
:request="table.requestFun" :request="table.requestFun"
:gridColumn="2" :gridColumn="2"
model="CARD" :params="queryParams"
:params="query.params.value"
:rowSelection="{ :rowSelection="{
selectedRowKeys: table._selectedRowKeys.value, selectedRowKeys: table._selectedRowKeys.value,
}" }"
:columns="columns"
@cancelSelect="table.cancelSelect" @cancelSelect="table.cancelSelect"
> >
<template #headerTitle> <template #headerTitle>
@ -56,7 +59,7 @@
<template #card="slotProps"> <template #card="slotProps">
<CardBox <CardBox
:value="slotProps" :value="slotProps"
:actions="[{ key: 1 }]" :actions="table.getActions(slotProps, 'card')"
v-bind="slotProps" v-bind="slotProps"
:active=" :active="
table._selectedRowKeys.value.includes(slotProps.id) table._selectedRowKeys.value.includes(slotProps.id)
@ -110,8 +113,52 @@
</j-col> </j-col>
</j-row> </j-row>
</template> </template>
<template #actions> <template #actions="item">
<PermissionButton <a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-dropdown
placement="bottomRight"
v-if="item.key === 'others'"
>
<a-button>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
v-for="(o, i) in item.children"
:key="i"
>
<a-button
type="link"
@click="o.onClick"
>
<AIcon :type="o.icon" />
<span>{{ o.text }}</span>
</a-button>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<PermissionButton
v-else
:uhasPermission="item.permission"
:tooltip="item.tooltip"
:pop-confirm="item.popConfirm"
@click="item.onClick"
:disabled="item.disabled"
>
<AIcon :type="item.icon" />
<span v-if="item.key !== 'delete'">{{
item.text
}}</span>
</PermissionButton>
</a-tooltip>
<!-- <PermissionButton
:uhasPermission="`${permission}:assert`" :uhasPermission="`${permission}:assert`"
@click="() => table.clickEdit(slotProps)" @click="() => table.clickEdit(slotProps)"
> >
@ -126,17 +173,50 @@
}" }"
> >
<AIcon type="DisconnectOutlined" /> <AIcon type="DisconnectOutlined" />
</PermissionButton> </PermissionButton> -->
</template> </template>
</CardBox> </CardBox>
</template> </template>
<template #permission="slotProps">
{{
table.permissionList.value.length &&
table.getPermissLabel(slotProps.permission)
}}
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
type="link"
:tooltip="i?.tooltip"
:pop-confirm="i.popConfirm"
@click="i.onClick"
:disabled="i?.disabled"
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</template>
</j-pro-table> </j-pro-table>
<div class="dialogs"> <div class="dialogs">
<AddDeviceOrProductDialog <AddDeviceOrProductDialog
v-if="dialogs.addShow" v-if="dialogs.addShow"
v-model:visible="dialogs.addShow" v-model:visible="dialogs.addShow"
:query-columns="query.columns" :query-columns="columns"
:parent-id="props.parentId" :parent-id="props.parentId"
:all-permission="table.permissionList.value" :all-permission="table.permissionList.value"
asset-type="product" asset-type="product"
@ -185,54 +265,70 @@ const emits = defineEmits(['openDeviceBind']);
const props = defineProps<{ const props = defineProps<{
parentId: string; parentId: string;
}>(); }>();
const query = {
columns: [ const columns = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
key: 'id', key: 'id',
ellipsis: true, ellipsis: true,
fixed: 'left', fixed: 'left',
search: { search: {
type: 'string', type: 'string',
},
}, },
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
}, },
}; {
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '资产权限',
dataIndex: 'permission',
key: 'permission',
ellipsis: true,
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
scopedSlots: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
fixed: 'right',
scopedSlots: true,
},
];
const queryParams = ref({});
const tableRef = ref(); const tableRef = ref();
const table = { const table = {
@ -250,6 +346,33 @@ const table = {
); );
}, },
getActions: (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
) => {
if (!data) return [];
else
return [
{
permission: true,
key: 'edit',
tooltip: { title: '编辑' },
icon: 'EditOutlined',
onClick: () => table.clickEdit(data),
},
{
permission: true,
key: 'unbind',
tooltip: { title: '解除绑定' },
popConfirm: {
title: `是否解除绑定`,
onConfirm: () => table.clickUnBind(data),
},
icon: 'DisconnectOutlined',
},
];
},
// //
getPermissionDict: () => { getPermissionDict: () => {
getPermissionDict_api().then((resp: any) => { getPermissionDict_api().then((resp: any) => {
@ -261,7 +384,7 @@ const table = {
const permissionList = table.permissionList.value; const permissionList = table.permissionList.value;
if (permissionList.length < 1 || values.length < 1) return ''; if (permissionList.length < 1 || values.length < 1) return '';
const result = values.map( const result = values.map(
(key) => permissionList.find((item:any) => item.id === key)?.name, (key) => permissionList.find((item: any) => item.id === key)?.name,
); );
return result.join(','); return result.join(',');
}, },
@ -442,6 +565,15 @@ const dialogs = reactive({
<style lang="less" scoped> <style lang="less" scoped>
.product-container { .product-container {
:deep(.ant-table-tbody) {
.ant-table-cell {
.ant-space-item {
.ant-btn-link {
padding: 0;
}
}
}
}
.card { .card {
.card-warp { .card-warp {
&.active { &.active {

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="basic-info-container"> <div class="basic-info-container">
<a-card> <div class="card">
<h3>基本信息</h3> <h3>基本信息</h3>
<a-form ref="basicFormRef" :model="form.data" class="basic-form"> <j-form ref="basicFormRef" :model="form.data" class="basic-form">
<div class="row" style="display: flex"> <div class="row" style="display: flex">
<a-form-item <j-form-item
label="菜单图标" label="菜单图标"
name="icon" name="icon"
:rules="[ :rules="[
@ -20,25 +20,28 @@
:type="form.data.icon" :type="form.data.icon"
style="font-size: 90px" style="font-size: 90px"
/> />
<span class="mark" @click="dialog.openDialog" <span class="mark" @click="dialogVisible = true"
>点击修改</span >点击修改</span
> >
</div> </div>
<div <div
v-else v-else
@click="dialog.openDialog" @click="dialogVisible = true"
class="icon-upload no-icon" class="icon-upload no-icon"
> >
<span> <span>
<plus-outlined style="font-size: 30px" /> <AIcon
type="PlusOutlined"
style="font-size: 30px"
/>
<p>点击选择图标</p> <p>点击选择图标</p>
</span> </span>
</div> </div>
</a-form-item> </j-form-item>
<a-row :gutter="24" style="flex: 1 1 auto"> <j-row :gutter="24" style="flex: 1 1 auto">
<a-col :span="12"> <j-col :span="12">
<a-form-item <j-form-item
label="名称" label="名称"
name="name" name="name"
:rules="[ :rules="[
@ -46,11 +49,11 @@
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
]" ]"
> >
<a-input v-model:value="form.data.name" /> <j-input v-model:value="form.data.name" />
</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="code" name="code"
:rules="[ :rules="[
@ -58,11 +61,11 @@
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
]" ]"
> >
<a-input v-model:value="form.data.code" /> <j-input v-model:value="form.data.code" />
</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="url" name="url"
:rules="[ :rules="[
@ -73,11 +76,11 @@
{ max: 128, message: '最多可输入128字符' }, { max: 128, message: '最多可输入128字符' },
]" ]"
> >
<a-input v-model:value="form.data.url" /> <j-input v-model:value="form.data.url" />
</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="sortIndex" name="sortIndex"
:rules="[ :rules="[
@ -87,80 +90,84 @@
}, },
]" ]"
> >
<a-input v-model:value="form.data.sortIndex" /> <j-input v-model:value="form.data.sortIndex" />
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
</div> </div>
<a-form-item label="说明" name="describe"> <j-form-item label="说明" name="describe">
<a-textarea <j-textarea
v-model:value="form.data.describe" v-model:value="form.data.describe"
:rows="4" :rows="4"
placeholder="请输入说明" placeholder="请输入说明"
/> />
</a-form-item> </j-form-item>
</a-form> </j-form>
</a-card> </div>
<a-card> <div class="card">
<h3>权限配置</h3> <h3>权限配置</h3>
<a-form <j-form
ref="permissFormRef" ref="permissFormRef"
:model="form.data" :model="form.data"
class="basic-form permiss-form" class="basic-form permiss-form"
> >
<a-form-item name="accessSupport" required> <j-form-item name="accessSupport" required>
<template #label> <template #label>
<span style="margin-right: 3px">数据权限控制</span> <span style="margin-right: 3px">数据权限控制</span>
<a-tooltip title="此菜单页面数据所对应的资产类型"> <j-tooltip title="此菜单页面数据所对应的资产类型">
<question-circle-outlined <AIcon
type="QuestionCircleOutlined"
class="img-style" class="img-style"
style="color: #a6a6a6" style="color: #a6a6a6"
/> />
</a-tooltip> </j-tooltip>
</template> </template>
<a-radio-group <j-radio-group
v-model:value="form.data.accessSupport" v-model:value="form.data.accessSupport"
name="radioGroup" name="radioGroup"
> >
<a-radio value="unsupported">不支持</a-radio> <j-radio value="unsupported">不支持</j-radio>
<a-radio value="support">支持</a-radio> <j-radio value="support">支持</j-radio>
<a-radio value="indirect"> <j-radio value="indirect">
<span style="margin-right: 3px">间接控制</span> <span style="margin-right: 3px">间接控制</span>
<a-tooltip <j-tooltip
title="此菜单内的数据基于其他菜单的数据权限控制" title="此菜单内的数据基于其他菜单的数据权限控制"
> >
<question-circle-filled class="img-style" /> <AIcon
</a-tooltip> type="QuestionCircleFilled"
</a-radio> class="img-style"
</a-radio-group> />
</j-tooltip>
</j-radio>
</j-radio-group>
<a-form-item <j-form-item
name="assetType" name="assetType"
v-if="form.data.accessSupport === 'support'" v-if="form.data.accessSupport === 'support'"
:rules="[{ required: true, message: '请选择资产类型' }]" :rules="[{ required: true, message: '请选择资产类型' }]"
style="margin-top: 24px; margin-bottom: 0" style="margin-top: 24px; margin-bottom: 0"
> >
<a-select <j-select
v-model:value="form.data.assetType" v-model:value="form.data.assetType"
style="width: 500px" style="width: 500px"
placeholder="请选择资产类型" placeholder="请选择资产类型"
> >
<a-select-option <j-select-option
v-for="item in form.assetsType" v-for="item in form.assetsType"
:value="item.value" :value="item.value"
>{{ item.label }}</a-select-option >{{ item.label }}</j-select-option
> >
</a-select> </j-select>
</a-form-item> </j-form-item>
<a-form-item <j-form-item
name="indirectMenus" name="indirectMenus"
v-if="form.data.accessSupport === 'indirect'" v-if="form.data.accessSupport === 'indirect'"
:rules="[{ required: true, message: '请选择关联菜单' }]" :rules="[{ required: true, message: '请选择关联菜单' }]"
style="margin-top: 24px; margin-bottom: 0" style="margin-top: 24px; margin-bottom: 0"
> >
<a-tree-select <j-tree-select
v-model:value="form.data.indirectMenus" v-model:value="form.data.indirectMenus"
style="width: 400px" style="width: 400px"
:dropdown-style="{ :dropdown-style="{
@ -177,18 +184,18 @@
value: 'id', value: 'id',
}" }"
> >
</a-tree-select> </j-tree-select>
</a-form-item> </j-form-item>
</a-form-item> </j-form-item>
<a-form-item label="权限"> <j-form-item label="权限">
<PermissChoose <PermissChoose
: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-form-item> </j-form-item>
</a-form> </j-form>
<PermissionButton <PermissionButton
type="primary" type="primary"
@ -197,22 +204,20 @@
> >
保存 保存
</PermissionButton> </PermissionButton>
</a-card> </div>
<!-- 弹窗 --> <!-- 弹窗 -->
<div class="dialogs"> <ChooseIconDialog
<ChooseIconDialog ref="ChooseIconRef" @confirm="dialog.confirm" /> v-if="dialogVisible"
</div> v-model:visible="dialogVisible"
@confirm="(typeStr:string)=>form.data.icon = typeStr"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import PermissionButton from '@/components/PermissionButton/index.vue'; import PermissionButton from '@/components/PermissionButton/index.vue';
import {
PlusOutlined,
QuestionCircleFilled,
QuestionCircleOutlined,
} from '@ant-design/icons-vue';
import { FormInstance, message } from 'ant-design-vue'; import { FormInstance, message } from 'ant-design-vue';
import ChooseIconDialog from '../components/ChooseIconDialog.vue'; import ChooseIconDialog from '../components/ChooseIconDialog.vue';
import PermissChoose from '../components/PermissChoose.vue'; import PermissChoose from '../components/PermissChoose.vue';
@ -260,9 +265,12 @@ const form = reactive({
// //
routeParams.id && routeParams.id &&
getMenuInfo_api(routeParams.id).then((resp: any) => { getMenuInfo_api(routeParams.id).then((resp: any) => {
console.log(1111);
form.data = { form.data = {
...(resp.result as formType), ...(resp.result as formType),
accessSupport: resp.result.accessSupport.value, accessSupport:
resp.result?.accessSupport?.value || 'unsupported',
}; };
}); });
// //
@ -323,15 +331,7 @@ const form = reactive({
form.init(); form.init();
// //
const ChooseIconRef = ref<any>(null); const dialogVisible = ref(false);
const dialog = {
openDialog: () => {
ChooseIconRef.value && ChooseIconRef.value.openDialog();
},
confirm: (typeStr: string) => {
form.data.icon = typeStr || form.data.icon;
},
};
type formType = { type formType = {
id?: string; id?: string;
@ -356,8 +356,10 @@ type assetType = {
<style lang="less" scoped> <style lang="less" scoped>
.basic-info-container { .basic-info-container {
.ant-card { .card {
margin-bottom: 24px; margin-bottom: 24px;
padding: 24px;
background-color: #fff;
h3 { h3 {
position: relative; position: relative;

View File

@ -11,18 +11,18 @@
<PermissionButton <PermissionButton
type="primary" type="primary"
:uhasPermission="`${permission}:update`" :uhasPermission="`${permission}:update`"
@click="dialog.openDialog('新增')" @click="openDialog('新增', {})"
> >
<AIcon type="PlusOutlined" />新增 <AIcon type="PlusOutlined" />新增
</PermissionButton> </PermissionButton>
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <j-space :size="16">
<PermissionButton <PermissionButton
type="link" type="link"
:uhasPermission="`${permission}:update`" :uhasPermission="`${permission}:update`"
:tooltip="{ title: '编辑' }" :tooltip="{ title: '编辑' }"
@click="dialog.openDialog('编辑', slotProps)" @click="openDialog('编辑', slotProps)"
> >
<AIcon type="EditOutlined" /> <AIcon type="EditOutlined" />
</PermissionButton> </PermissionButton>
@ -30,7 +30,7 @@
type="link" type="link"
:uhasPermission="true" :uhasPermission="true"
:tooltip="{ title: '查看' }" :tooltip="{ title: '查看' }"
@click="dialog.openDialog('查看', slotProps)" @click="openDialog('查看', slotProps)"
> >
<AIcon type="SearchOutlined" /> <AIcon type="SearchOutlined" />
</PermissionButton> </PermissionButton>
@ -45,15 +45,18 @@
> >
<AIcon type="DeleteOutlined" /> <AIcon type="DeleteOutlined" />
</PermissionButton> </PermissionButton>
</a-space> </j-space>
</template> </template>
</j-pro-table> </j-pro-table>
<div class="dialog"> <div class="dialog">
<ButtonAddDialog <ButtonAddDialog
ref="dialogRef" v-if="dialogVisible"
@confirm="dialog.confirm" v-model:visible="dialogVisible"
:menu-info="menuInfo" :menu-info="menuInfo"
:mode="dialogTitle"
:data="selectItem"
@confirm="table.getList"
/> />
</div> </div>
</div> </div>
@ -76,15 +79,13 @@ const routeParams = {
}; };
// //
const dialogRef = ref<any>(null); const selectItem = ref<any>({});
const dialog = { const dialogVisible = ref(false);
// const dialogTitle = ref<'查看' | '新增' | '编辑'>('新增');
openDialog: (mode: string, row?: object) => { const openDialog = (mode: '查看' | '新增' | '编辑', row: object) => {
dialogRef.value && dialogRef.value.openDialog(mode, { ...row }); selectItem.value = { ...row };
}, dialogTitle.value = mode;
confirm: () => { dialogVisible.value = true;
table.getList();
},
}; };
// - // -
const menuInfo = ref<any>({}); const menuInfo = ref<any>({});

View File

@ -1,14 +1,14 @@
<template> <template>
<page-container> <page-container>
<div class="menu-detail-container"> <div class="menu-detail-container">
<a-tabs v-model:activeKey="activeKey"> <j-tabs v-model:activeKey="activeKey">
<a-tab-pane key="basic" tab="基本信息"> <j-tab-pane key="basic" tab="基本信息">
<BasicInfo /> <BasicInfo />
</a-tab-pane> </j-tab-pane>
<a-tab-pane key="button" tab="按钮管理"> <j-tab-pane key="button" tab="按钮管理">
<ButtonMange /> <ButtonMange />
</a-tab-pane> </j-tab-pane>
</a-tabs> </j-tabs>
</div> </div>
</page-container> </page-container>
</template> </template>

View File

@ -1,13 +1,12 @@
<template> <template>
<a-modal <a-modal
v-model:visible="dialog.visible" visible
:title="form.mode" :title="props.mode"
width="660px" width="660px"
@ok="dialog.handleOk" @ok="confirm"
@cancel="emits('update:visible', false)"
:maskClosable="false" :maskClosable="false"
cancelText="取消" :confirmLoading="loading"
okText="确定"
:confirmLoading="dialog.loading"
> >
<a-form :model="form.data" class="basic-form" ref="formRef"> <a-form :model="form.data" class="basic-form" ref="formRef">
<a-form-item <a-form-item
@ -20,7 +19,7 @@
> >
<a-input <a-input
v-model:value="form.data.id" v-model:value="form.data.id"
:disabled="form.mode !== '新增'" :disabled="props.mode !== '新增'"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -33,7 +32,7 @@
> >
<a-input <a-input
v-model:value="form.data.name" v-model:value="form.data.name"
:disabled="form.mode === '查看'" :disabled="props.mode === '查看'"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -47,19 +46,11 @@
}, },
]" ]"
> >
<!-- <a-form-item-rest>
<PermissChoose
:first-width="8"
max-height="350px"
v-model:value="form.data.permissions"
:disabled="form.mode === '查看'"
/>
</a-form-item-rest> -->
<PermissChoose <PermissChoose
:first-width="8" :first-width="8"
max-height="350px" max-height="350px"
v-model:value="form.data.permissions" v-model:value="form.data.permissions"
:disabled="form.mode === '查看'" :disabled="props.mode === '查看'"
:key="form.data.id || ''" :key="form.data.id || ''"
/> />
</a-form-item> </a-form-item>
@ -68,7 +59,7 @@
v-model:value="form.data.describe" v-model:value="form.data.describe"
:rows="4" :rows="4"
placeholder="请输入说明" placeholder="请输入说明"
:disabled="form.mode === '查看'" :disabled="props.mode === '查看'"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -81,54 +72,42 @@ import { Rule } from 'ant-design-vue/es/form';
import PermissChoose from '../components/PermissChoose.vue'; import PermissChoose from '../components/PermissChoose.vue';
import { saveMenuInfo_api } from '@/api/system/menu'; import { saveMenuInfo_api } from '@/api/system/menu';
const emits = defineEmits(['confirm', 'update:visible']);
const props = defineProps<{ const props = defineProps<{
menuInfo: { menuInfo: {
buttons: formType[]; buttons: formType[];
id: string; id: string;
}; };
visible: boolean;
mode: '查看' | '新增' | '编辑';
data: formType;
}>(); }>();
const emits = defineEmits(['confirm']);
const dialog = reactive({
visible: false,
loading: false,
handleOk: () => {
props.menuInfo.id &&
formRef.value &&
formRef.value
.validate()
.then(() => {
const buttons = toRaw(props.menuInfo.buttons);
const button = buttons.find(
(item) => item.id === form.data.id,
);
if (button) {
Object.entries(form.data).forEach(([key, value]) => {
button[key] = value;
});
} else buttons.push({ ...form.data });
const params = {
...props.menuInfo,
buttons,
};
dialog.loading = true;
saveMenuInfo_api(params)
.then((resp) => {
dialog.changeVisible();
message.success('操作成功');
emits('confirm');
})
.finally(() => (dialog.loading = false));
})
.catch(() => {});
},
changeVisible: (mode?: string, formValue?: formType) => {
dialog.visible = !dialog.visible;
form.data = { ...initForm, ...formValue };
form.mode = mode || '';
formRef.value && formRef.value.clearValidate(); const loading = ref(false);
}, const confirm = () => {
}); loading.value = true;
formRef.value &&
formRef.value.validate().then(() => {
const buttons = toRaw(props.menuInfo.buttons);
const button = buttons.find((item) => item.id === form.data.id);
if (button) {
Object.entries(form.data).forEach(([key, value]) => {
button[key] = value;
});
} else buttons.push({ ...form.data });
const params = {
...props.menuInfo,
buttons,
};
saveMenuInfo_api(params)
.then((resp) => {
message.success('操作成功');
emits('confirm');
emits('update:visible', false);
})
.finally(() => (loading.value = false));
});
};
const initForm = { const initForm = {
name: '', name: '',
id: '', id: '',
@ -137,19 +116,13 @@ const initForm = {
} as formType; } as formType;
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const form = reactive({ const form = reactive({
data: { ...initForm }, data: { ...initForm, ...props.data },
mode: '', checkPermission: (_rule: Rule, value: string[]) => {
checkPermission: async (_rule: Rule, value: string[]) => {
if (!value || value.length < 1) return Promise.reject('请选择权限'); if (!value || value.length < 1) return Promise.reject('请选择权限');
return Promise.resolve(); return Promise.resolve();
}, },
}); });
//
defineExpose({
openDialog: dialog.changeVisible,
});
type formType = { type formType = {
name: string; name: string;
id: string; id: string;

View File

@ -1,24 +1,33 @@
<template> <template>
<a-modal <j-modal
v-model:visible="dialog.visible" visible
title="菜单图标" title="菜单图标"
width="800px" width="800px"
@ok="dialog.handleOk" @cancel="emits('update:visible', false)"
@ok="confirm"
> >
<a-radio-group v-model:value="selected" class="radio"> <j-radio-group v-model:value="selected" class="radio">
<a-radio-button <j-radio-button
v-for="typeStr in iconKeys" v-for="typeStr in iconKeys"
:value="typeStr" :value="typeStr"
:class="{ active: selected === typeStr }" :class="{ active: selected === typeStr }"
> >
<a-icon :type="typeStr" /> <AIcon :type="typeStr" />
</a-radio-button> </j-radio-button>
</a-radio-group> </j-radio-group>
</a-modal> </j-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const emits = defineEmits(['confirm']); const emits = defineEmits(['confirm', 'update:visible']);
const props = defineProps<{
visible: boolean;
}>();
const confirm = () => {
emits('confirm', selected.value);
emits('update:visible', false);
};
const iconKeys = [ const iconKeys = [
'EyeOutlined', 'EyeOutlined',
'EditOutlined', 'EditOutlined',
@ -48,29 +57,11 @@ const iconKeys = [
'TeamOutlined', 'TeamOutlined',
'MenuUnfoldOutlined', 'MenuUnfoldOutlined',
'MenuFoldOutlined', 'MenuFoldOutlined',
'QuestionCircleOutlined',
'InfoCircleOutlined', 'InfoCircleOutlined',
'SearchOutlined', 'SearchOutlined',
]; ];
const dialog = reactive({
visible: false,
handleOk: () => {
emits('confirm', selected.value);
dialog.changeVisible();
selected.value = '';
},
changeVisible: (show?: boolean) => {
dialog.visible = show === undefined ? !dialog.visible : show;
},
});
const selected = ref<string>(''); const selected = ref<string>('');
//
defineExpose({
openDialog: dialog.changeVisible,
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -155,10 +155,12 @@ const permission = reactive({
const checked = checkedValue?.find( const checked = checkedValue?.find(
(checkedItem) => checkedItem.permission === item.id, (checkedItem) => checkedItem.permission === item.id,
); );
const options = item.actions.map((actionItem: any) => ({
label: actionItem.name, const options =
value: actionItem.action, item.actions && item.actions.map((actionItem: any) => ({
})); label: actionItem.name,
value: actionItem.action,
})) || [];
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@ -182,7 +184,6 @@ const permission = reactive({
}); });
permission.init(); permission.init();
type permissionType = { type permissionType = {
id: string; id: string;
name: string; name: string;

View File

@ -1,14 +1,18 @@
<template> <template>
<page-container> <page-container>
<div class="menu-container"> <div class="menu-container">
<Search :columns="query.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="table.columns" :columns="columns"
:request="table.getList" :request="table.getList"
model="TABLE" model="TABLE"
:params="query.params" :params="queryParams"
noPagination
> >
<template #headerTitle> <template #headerTitle>
<PermissionButton <PermissionButton
@ -18,17 +22,11 @@
> >
<AIcon type="PlusOutlined" />新增 <AIcon type="PlusOutlined" />新增
</PermissionButton> </PermissionButton>
<a-button <j-button
style="margin-left: 12px" style="margin-left: 12px"
@click="router.push('/system/Menu/Setting')" @click="router.push('/system/Menu/Setting')"
>菜单配置</a-button >菜单配置</j-button
> >
<!-- <PermissionButton
:uhasPermission="true"
@click="router.push('/system/Menu/Setting')"
>
菜单配置
</PermissionButton> -->
</template> </template>
<template #createTime="slotProps"> <template #createTime="slotProps">
{{ {{
@ -38,17 +36,17 @@
}} }}
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <j-space :size="16">
<a-tooltip> <j-tooltip>
<template #title>查看</template> <template #title>查看</template>
<a-button <j-button
style="padding: 0" style="padding: 0"
type="link" type="link"
@click="table.toDetails(slotProps)" @click="table.toDetails(slotProps)"
> >
<search-outlined /> <AIcon type="SearchOutlined" />
</a-button> </j-button>
</a-tooltip> </j-tooltip>
<PermissionButton <PermissionButton
type="link" type="link"
@ -69,7 +67,7 @@
> >
<AIcon type="DeleteOutlined" /> <AIcon type="DeleteOutlined" />
</PermissionButton> </PermissionButton>
</a-space> </j-space>
</template> </template>
</j-pro-table> </j-pro-table>
</div> </div>
@ -80,7 +78,6 @@
import PermissionButton from '@/components/PermissionButton/index.vue'; import PermissionButton from '@/components/PermissionButton/index.vue';
import { getMenuTree_api, delMenuInfo_api } from '@/api/system/menu'; import { getMenuTree_api, delMenuInfo_api } from '@/api/system/menu';
import { SearchOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import moment from 'moment'; import moment from 'moment';
@ -88,112 +85,75 @@ const permission = 'system/Menu';
const router = useRouter(); const router = useRouter();
// const columns = [
const query = reactive({ {
columns: [ title: '编码',
{ dataIndex: 'code',
title: '编码', key: 'code',
dataIndex: 'code', ellipsis: true,
key: 'code', fixed: 'left',
ellipsis: true, search: {
fixed: 'left', type: 'string',
search: {
type: 'string',
},
}, },
{ width: 300,
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '页面地址',
dataIndex: 'url',
key: 'url',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '排序',
dataIndex: 'sortIndex',
key: 'sortIndex',
ellipsis: true,
search: {
type: 'number',
},
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
ellipsis: true,
search: {
type: 'date',
},
},
],
params: {
terms: [],
}, },
search: (params: any) => { {
query.params = params; title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
width: 220,
}, },
}); {
title: '页面地址',
dataIndex: 'url',
key: 'url',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '排序',
dataIndex: 'sortIndex',
key: 'sortIndex',
ellipsis: true,
search: {
type: 'number',
},
width: 80,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
width: 200,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
ellipsis: true,
search: {
type: 'date',
},
width: 180,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
width: 140,
},
];
const queryParams = ref({ terms: [] });
const tableRef = ref<Record<string, any>>({}); // const tableRef = ref<Record<string, any>>({}); //
const table = reactive({ const table = reactive({
columns: [
{
title: '编码',
dataIndex: 'code',
key: 'code',
width: 300,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 220,
},
{
title: '页面地址',
dataIndex: 'url',
key: 'url',
},
{
title: '排序',
dataIndex: 'sortIndex',
key: 'sortIndex',
width: 80,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
width: 200,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
scopedSlots: true,
width: 180,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
width: 140,
},
],
tableData: [],
total: 0, total: 0,
getList: async (_params: any) => { getList: async (_params: any) => {
// //
@ -220,7 +180,7 @@ const table = reactive({
..._params, ..._params,
terms: terms:
_params.terms && _params.length !== 0 _params.terms && _params.length !== 0
? [...query.params.terms, item] ? [..._params.terms, item]
: [item], : [item],
sorts: [{ name: 'sortIndex', order: 'asc' }], sorts: [{ name: 'sortIndex', order: 'asc' }],
paging: false, paging: false,
@ -233,9 +193,9 @@ const table = reactive({
code: resp.message, code: resp.message,
result: { result: {
data: resp.result, data: resp.result,
pageIndex: 0, pageIndex: resp.pageIndex,
pageSize: 0, pageSize: resp.pageSize,
total: 0, total: resp.total,
}, },
status: resp.status, status: resp.status,
}; };

View File

@ -1,42 +1,42 @@
<template> <template>
<div class="role-permiss-container"> <div class="role-permiss-container">
<a-card> <section class="card">
<h5>基本信息</h5> <h5>基本信息</h5>
<a-form ref="formRef" class="basic-form" :model="form.data" layout="vertical"> <j-form ref="formRef" class="basic-form" :model="form.data" layout="vertical">
<a-form-item <j-form-item
name="name" name="name"
label="名称" label="名称"
:rules="[{ required: true, message: '请输入名称' }]" :rules="[{ required: true, message: '请输入名称' }]"
> >
<a-input <j-input
v-model:value="form.data.name" v-model:value="form.data.name"
placeholder="请输入角色名称" placeholder="请输入角色名称"
:maxlength="64" :maxlength="64"
/> />
</a-form-item> </j-form-item>
<a-form-item name="name" label="说明"> <j-form-item name="name" label="说明">
<a-textarea <j-textarea
v-model:value="form.data.description" v-model:value="form.data.description"
placeholder="请输入说明" placeholder="请输入说明"
:maxlength="200" :maxlength="200"
show-count show-count
/> />
</a-form-item> </j-form-item>
</a-form> </j-form>
</a-card> </section>
<a-card> <section class="card">
<h5>权限分配</h5> <h5>权限分配</h5>
<PermissTree v-model:select-items="form.menus" /> <PermissTree v-model:select-items="form.menus" />
<a-button <j-button
type="primary" type="primary"
:disabled="form.loading" :disabled="form.loading"
@click="form.clickSave" @click="form.clickSave"
style="margin-top: 24px;" style="margin-top: 24px;"
>保存</a-button >保存</j-button
> >
</a-card> </section>
</div> </div>
</template> </template>
@ -86,8 +86,10 @@ form.getForm();
<style lang="less" scoped> <style lang="less" scoped>
.role-permiss-container { .role-permiss-container {
.ant-card { .card {
margin-bottom: 24px; margin-bottom: 24px;
background-color: #fff;
padding: 24px;
h5 { h5 {
position: relative; position: relative;

View File

@ -1,134 +1,149 @@
<template> <template>
<a-card class="role-user-container"> <div class="role-user-container">
<Search :columns="query.columns" /> <j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table <j-pro-table
ref="tableRef" ref="tableRef"
:columns="table.columns" :columns="columns"
:request="getUserByRole_api" :request="table.getList"
model="TABLE" model="TABLE"
:defaultParams="query.params" :params="queryParams"
:rowSelection="{ :rowSelection="{
selectedRowKeys: table._selectedRowKeys, selectedRowKeys: selectedRowKeys,
onChange: table.onSelectChange, onChange: (keys:string[])=>selectedRowKeys = keys,
}" }"
@cancelSelect="table.cancelSelect" @cancelSelect="selectedRowKeys = []"
size="small"
> >
<template #headerTitle> <template #headerTitle>
<a-button type="primary" @click="table.clickAdd" <j-button type="primary" @click="dialogVisible = true">
><plus-outlined />新增</a-button <AIcon type="PlusOutlined" />新增
> </j-button>
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.status"
:text="slotProps.status ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
></BadgeStatus>
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <j-space :size="16">
<a-popconfirm <PermissionButton
title="确认解绑" type="link"
ok-text="确定" :tooltip="{ title: '解绑' }"
cancel-text="取消" :pop-confirm="{
@confirm="table.clickUnBind(slotProps)" title: `确认解绑`,
onConfirm: () => table.unbind([slotProps.id]),
}"
> >
<a-tooltip> <AIcon type="DisconnectOutlined" />
<template #title>解绑</template> </PermissionButton>
<a-button style="padding: 0" type="link"> </j-space>
<disconnect-outlined />
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template> </template>
</j-pro-table> </j-pro-table>
<div class="dialogs"> <AddUserDialog
<AddUserDialog :open="dialog.openAdd" @refresh="table.refresh" /> v-if="dialogVisible"
</div> v-model:visible="dialogVisible"
</a-card> :role-id="roleId"
@refresh="table.refresh"
/>
</div>
</template> </template>
<script setup lang="ts" name="RoleUser"> <script setup lang="ts" name="RoleUser">
import { PlusOutlined, DisconnectOutlined } from '@ant-design/icons-vue'; import PermissionButton from '@/components/PermissionButton/index.vue';
import AddUserDialog from '../components/AddUserDialog.vue'; import AddUserDialog from '../components/AddUserDialog.vue';
import { getUserByRole_api, unbindUser_api } from '@/api/system/role'; import { getUserByRole_api, unbindUser_api } from '@/api/system/role';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import userType from './index';
const route = useRoute(); const roleId = useRoute().params.id as string;
const roleId = route.params.id as string;
const query = reactive<userType.queryType>({ const columns = [
columns: [ {
{ title: '姓名',
title: '姓名', dataIndex: 'name',
dataIndex: 'name', key: 'name',
key: 'name', search: {
type: 'string',
}, },
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
],
params: {
terms: [
{
terms: [
{
column: 'id$in-dimension$role',
value: route.params.id,
},
],
},
],
}, },
}); {
title: '用户名',
dataIndex: 'username',
key: 'username',
search: {
type: 'string',
},
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
scopedSlots: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: '200px',
scopedSlots: true,
},
];
const queryParams = ref({});
const tableRef = ref<Record<string, any>>({}); const tableRef = ref<Record<string, any>>({});
const table = reactive<userType.tableType>({ const selectedRowKeys = ref<string[]>([]);
columns: [ const table = {
{ getList: (oParams: any) => {
title: '姓名', const params = {
dataIndex: 'name', ...oParams,
key: 'name', terms: [
}, {
{ terms: [
title: '用户名', {
dataIndex: 'username', column: 'id$in-dimension$role',
key: 'username', value: roleId,
}, },
{ ],
title: '创建时间', },
dataIndex: 'createTime', ],
key: 'createTime', };
}, if (oParams.terms[0])
{ params.terms.unshift({
title: '状态', terms: oParams.terms[0].terms,
dataIndex: 'status', });
key: 'status', return getUserByRole_api(params);
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
],
tableData: [],
//
clickAdd: () => {
dialog.openAdd += 1;
},
//
clickUnBind: (row: any) => {
table.unbind([row.id]);
}, },
// //
unbind: (ids: string[] = []) => { unbind: (ids: string[] = []) => {
@ -143,20 +158,24 @@ const table = reactive<userType.tableType>({
refresh: () => { refresh: () => {
tableRef.value.reload(); tableRef.value.reload();
}, },
// };
_selectedRowKeys: [] as string[],
onSelectChange: (keys: string[]) => {
table._selectedRowKeys = [...keys];
},
cancelSelect: () => {
table._selectedRowKeys = [];
},
});
// //
const dialog = reactive({ const dialogVisible = ref(false);
openAdd: 0,
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.role-user-container {
background-color: #fff;
:deep(.ant-table-tbody) {
.ant-table-cell {
.ant-space-item {
.ant-btn-link {
padding: 0;
}
}
}
}
}
</style>

View File

@ -1,123 +1,99 @@
<template> <template>
<a-modal <j-modal
v-model:visible="dialog.visible" visible
title="新增" title="新增"
width="1000px" width="1000px"
@ok="dialog.handleOk" @ok="confirm"
@cancel="emits('update:visible', false)"
> >
<Search :columns="query.columns" type="simple" /> <j-advanced-search
:columns="columns"
type="simple"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table <j-pro-table
ref="tableRef" ref="tableRef"
:columns="table.columns" :columns="columns"
:request="getUserByRole_api" :request="getUserList"
model="TABLE" model="TABLE"
:params="query.params" :params="queryParams"
:rowSelection="{ :rowSelection="{
selectedRowKeys: table._selectedRowKeys, selectedRowKeys: selectedRowKeys,
onChange: table.onSelectChange, onChange: (keys:string[])=>selectedRowKeys = keys,
}" }"
@cancelSelect="table.cancelSelect" @cancelSelect="selectedRowKeys = []"
> >
</j-pro-table> </j-pro-table>
</j-modal>
<template #footer>
<a-button key="back" @click="dialog.visible = false">取消</a-button>
<a-button key="submit" type="primary" @click="dialog.handleOk"
>确定</a-button
>
</template>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getUserByRole_api, bindUser_api } from '@/api/system/role'; import { getUserByRole_api, bindUser_api } from '@/api/system/role';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
const route = useRoute();
const emits = defineEmits(['refresh']); const emits = defineEmits(['refresh', 'update:visible']);
const props = defineProps({ const props = defineProps<{
open: Number, visible: boolean;
}); roleId: string
}>();
const query = reactive({ const columns = [
columns: [ {
{ title: '姓名',
title: '姓名', dataIndex: 'name',
dataIndex: 'name', key: 'name',
key: 'name', search: {
type: 'string',
}, },
{ },
title: '用户名', {
dataIndex: 'username', title: '用户名',
key: 'username', dataIndex: 'username',
key: 'username',
search: {
type: 'string',
}, },
], },
params: { ];
const queryParams = ref({});
const selectedRowKeys = ref<string[]>([]);
const getUserList = (oParams: any) => {
const params = {
...oParams,
sorts: [{ name: 'createTime', order: 'desc' }], sorts: [{ name: 'createTime', order: 'desc' }],
terms: [ terms: [
{ {
terms: [ terms: [
{ {
column: 'id$in-dimension$role$not', column: 'id$in-dimension$role$not',
value: route.params.id, value: props.roleId,
}, },
], ],
}, },
], ],
}, };
}); if (oParams.terms[0])
const tableRef = ref<Record<string, any>>({}); params.terms.unshift({
const table = reactive({ terms: oParams.terms[0].terms,
_selectedRowKeys: [] as string[], });
columns: [ return getUserByRole_api(params);
{ };
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
],
tableData: [],
onSelectChange: (keys: string[]) => {
table._selectedRowKeys = [...keys];
},
cancelSelect: () => {
table._selectedRowKeys = [];
},
});
// const confirm = () => {
const dialog = reactive({ if (selectedRowKeys.value.length < 1) {
visible: false, message.error('请至少选择一项');
handleOk: () => { } else {
if (table._selectedRowKeys.length < 1) { bindUser_api(props.roleId, selectedRowKeys.value).then(
message.error('请至少选择一项'); (resp) => {
} else {
bindUser_api(
route.params.id as string,
table._selectedRowKeys,
).then((resp) => {
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功'); message.success('操作成功');
emits('refresh'); emits('refresh');
dialog.visible = false; emits('update:visible', false);
} }
}); },
} );
}, }
}); };
watch(
() => props.open,
() => {
dialog.visible = true;
},
);
</script> </script>
<style scoped></style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="permiss-tree-container"> <div class="permiss-tree-container">
<a-table <j-table
:columns="columns" :columns="columns"
:data-source="tableData" :data-source="tableData"
:pagination="false" :pagination="false"
@ -10,28 +10,28 @@
<!-- 表头 --> <!-- 表头 -->
<template #headerCell="{ column }"> <template #headerCell="{ column }">
<div v-if="column.key === 'menu'"> <div v-if="column.key === 'menu'">
<a-checkbox <j-checkbox
v-model:checked="selectedAll" v-model:checked="selectedAll"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@change="selectAllChange" @change="selectAllChange"
>菜单权限</a-checkbox >菜单权限</j-checkbox
> >
</div> </div>
<div v-else-if="column.key === 'data'"> <div v-else-if="column.key === 'data'">
<span style="">数据权限</span> <span style="">数据权限</span>
<a-tooltip> <j-tooltip>
<template #title <template #title
>勾选任意数据权限均能看到自己创建的数据权限</template >勾选任意数据权限均能看到自己创建的数据权限</template
> >
<question-circle-outlined /> <AIcon type="QuestionCircleOutlined" />
</a-tooltip> </j-tooltip>
<a-checkbox <j-checkbox
v-model:checked="bulkShow" v-model:checked="bulkShow"
@change="bulkValue = ''" @change="bulkValue = ''"
style="margin-left: 10px" style="margin-left: 10px"
>批量设置</a-checkbox >批量设置</j-checkbox
> >
<a-select <j-select
v-show="bulkShow" v-show="bulkShow"
v-model:value="bulkValue" v-model:value="bulkValue"
:size="'middle'" :size="'middle'"
@ -39,7 +39,7 @@
:options="bulkOptions" :options="bulkOptions"
@change="bulkChange" @change="bulkChange"
placeholder="请选择" placeholder="请选择"
></a-select> ></j-select>
</div> </div>
<div v-else> <div v-else>
<span>{{ column.title }}</span> <span>{{ column.title }}</span>
@ -48,21 +48,21 @@
<!-- 表格内容 --> <!-- 表格内容 -->
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<div v-if="column.key === 'menu'"> <div v-if="column.key === 'menu'">
<a-checkbox <j-checkbox
v-model:checked="record.granted" v-model:checked="record.granted"
:indeterminate="record.indeterminate" :indeterminate="record.indeterminate"
@change="menuChange(record, true)" @change="menuChange(record, true)"
>{{ record.name }}</a-checkbox >{{ record.name }}</j-checkbox
> >
</div> </div>
<div v-else-if="column.key === 'action'"> <div v-else-if="column.key === 'action'">
<div v-if="record.buttons && record.buttons.length > 0"> <div v-if="record.buttons && record.buttons.length > 0">
<a-checkbox <j-checkbox
v-for="button in record.buttons" v-for="button in record.buttons"
v-model:checked="button.granted" v-model:checked="button.granted"
@change="actionChange(record)" @change="actionChange(record)"
>{{ button.name }}</a-checkbox >{{ button.name }}</j-checkbox
> >
</div> </div>
</div> </div>
@ -72,13 +72,16 @@
不支持数据权限配置默认可查看全部数据 不支持数据权限配置默认可查看全部数据
</span> </span>
<div v-else-if="record.accessSupport.value === 'support'"> <div v-else-if="record.accessSupport.value === 'support'">
<a-radio-group v-model:value="record.selectAccesses"> <j-radio-group
<a-radio v-model:value="record.selectAccesses"
@change="resetBulk"
>
<j-radio
:value="asset.supportId" :value="asset.supportId"
v-for="asset in record.assetAccesses" v-for="asset in record.assetAccesses"
>{{ asset.name }}</a-radio >{{ asset.name }}</j-radio
> >
</a-radio-group> </j-radio-group>
</div> </div>
<span <span
v-else-if=" v-else-if="
@ -89,12 +92,11 @@
> >
</div> </div>
</template> </template>
</a-table> </j-table>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { getPrimissTree_api } from '@/api/system/role'; import { getPrimissTree_api } from '@/api/system/role';
@ -167,6 +169,12 @@ const bulkChange = () => {
} }
}); });
}; };
//
const resetBulk = () => {
bulkValue.value = '';
bulkShow.value = false;
};
// ------------------------------ // ------------------------------
const flatTableData: tableItemType[] = []; // -- 便 const flatTableData: tableItemType[] = []; // -- 便
@ -273,6 +281,9 @@ function menuChange(
} }
} }
//
resetBulk();
// //
const selectList = flatTableData.filter((item) => item.granted); // const selectList = flatTableData.filter((item) => item.granted); //
if (selectList.length === flatTableData.length) { if (selectList.length === flatTableData.length) {
@ -390,8 +401,3 @@ type tableItemType = {
assetAccesses?: any[]; assetAccesses?: any[];
}; };
</script> </script>
<style lang="less" scoped>
.permiss-tree-container {
}
</style>

View File

@ -1,10 +1,10 @@
<template> <template>
<page-container> <page-container>
<div class="details-container"> <div class="details-container">
<a-tabs v-model:activeKey="activeKey"> <j-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="权限分配"><Permiss /></a-tab-pane> <j-tab-pane key="1" tab="权限分配"><Permiss /></j-tab-pane>
<a-tab-pane key="2" tab="用户管理"><User /></a-tab-pane> <j-tab-pane key="2" tab="用户管理"><User /></j-tab-pane>
</a-tabs> </j-tabs>
</div> </div>
</page-container> </page-container>
</template> </template>
@ -12,7 +12,6 @@
<script setup lang="ts" name="Detail"> <script setup lang="ts" name="Detail">
import Permiss from './Permiss/index.vue'; import Permiss from './Permiss/index.vue';
import User from './User/index.vue'; import User from './User/index.vue';
const route = useRoute();
const activeKey = ref('1'); const activeKey = ref('1');
</script> </script>

View File

@ -1,18 +1,20 @@
<template> <template>
<a-modal <a-modal
v-model:visible="dialog.visible" visible
title="新增" title="新增"
width="670px" width="670px"
@ok="dialog.handleOk" @cancel="emits('update:visible', false)"
@ok="confirm"
:confirm-loading="loading"
> >
<a-form ref="formRef" :model="form.data" layout="vertical"> <a-form ref="formRef" :model="form" layout="vertical">
<a-form-item <a-form-item
name="name" name="name"
label="名称" label="名称"
:rules="[{ required: true, message: '请输入名称' }]" :rules="[{ required: true, message: '请输入名称' }]"
> >
<a-input <a-input
v-model:value="form.data.name" v-model:value="form.name"
placeholder="请输入角色名称" placeholder="请输入角色名称"
allow-clear allow-clear
:maxlength="64" :maxlength="64"
@ -20,72 +22,52 @@
</a-form-item> </a-form-item>
<a-form-item name="name" label="说明"> <a-form-item name="name" label="说明">
<a-textarea <a-textarea
v-model:value="form.data.description" v-model:value="form.description"
placeholder="请输入说明" placeholder="请输入说明"
allow-clear allow-clear
:maxlength="200" :maxlength="200"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
<template #footer>
<a-button key="back" @click="dialog.visible = false">取消</a-button>
<a-button
key="submit"
type="primary"
:loading="form.loading"
@click="dialog.handleOk"
>确定</a-button
>
</template>
</a-modal> </a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, message } from 'ant-design-vue'; import { FormInstance, message } from 'ant-design-vue';
import { saveRole_api } from '@/api/system/role'; import { saveRole_api } from '@/api/system/role';
const router = useRouter(); import { useMenuStore } from '@/store/menu';
const route = useRoute(); const route = useRoute();
const { jumpPage } = useMenuStore();
const emits = defineEmits(['update:visible']);
const props = defineProps<{
visible: boolean;
}>();
// //
const dialog = reactive({ const loading = ref(false);
visible: false, const form = ref<any>({});
handleOk: () => {
formRef.value
?.validate()
.then(() => saveRole_api(form.data))
.then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
dialog.visible = false;
if (route.query.save) {
// @ts-ignore
window?.onSaveSuccess && window.onSaveSuccess(resp.result.id);
window.close();
} else router.push(`/system/Role/detail/${resp.result.id}`);
}
});
},
//
changeVisible: (status: boolean, defaultForm: object = {}) => {
dialog.visible = status;
form.data = { name: '', description: '', ...defaultForm };
},
});
//
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const form = reactive({
loading: false,
data: {
name: '',
description: '',
},
});
// const confirm = () => {
defineExpose({ loading.value = true;
openDialog: dialog.changeVisible, formRef.value
}); ?.validate()
.then(() => saveRole_api(form.value))
.then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('update:visible', false);
if (route.query.save) {
// @ts-ignore
window?.onSaveSuccess(resp.result.id);
window.close();
} else jumpPage(`system/Role/detail`, { id: resp.result.id });
}
})
.finally(() => (loading.value = false));
};
//
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,20 +1,23 @@
<template> <template>
<page-container> <page-container>
<a-card class="role-container"> <a-card class="role-container">
<Search :columns="query.columns" /> <j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = params"
/>
<j-pro-table <j-pro-table
ref="tableRef" ref="tableRef"
:columns="table.columns" :columns="columns"
:request="getRoleList_api" :request="getRoleList_api"
model="TABLE" model="TABLE"
:params="query.params" :params="queryParams"
> >
<template #headerTitle> <template #headerTitle>
<PermissionButton <PermissionButton
type="primary" type="primary"
:uhasPermission="`${permission}:add`" :uhasPermission="`${permission}:add`"
@click="table.clickAdd" @click="dialogVisible = true"
> >
<AIcon type="PlusOutlined" />新增 <AIcon type="PlusOutlined" />新增
</PermissionButton> </PermissionButton>
@ -47,9 +50,7 @@
</template> </template>
</j-pro-table> </j-pro-table>
<div class="dialogs"> <AddDialog v-if="dialogVisible" v-model:visible="dialogVisible" />
<AddDialog ref="addDialogRef" />
</div>
</a-card> </a-card>
</page-container> </page-container>
</template> </template>
@ -59,73 +60,56 @@ import PermissionButton from '@/components/PermissionButton/index.vue';
import AddDialog from './components/AddDialog.vue'; import AddDialog from './components/AddDialog.vue';
import { getRoleList_api, delRole_api } from '@/api/system/role'; import { getRoleList_api, delRole_api } from '@/api/system/role';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useMenuStore } from '@/store/menu';
const permission = 'system/Role'; const permission = 'system/Role';
const { jumpPage } = useMenuStore();
const addDialogRef = ref(); // const addDialogRef = ref(); //
const router = useRouter(); const isSave = !!useRoute().query.save;
const route = useRoute();
// const columns = [
const query = reactive({ {
columns: [ title: '标识',
{ dataIndex: 'id',
title: '标识', key: 'id',
dataIndex: 'id', ellipsis: true,
key: 'id', fixed: 'left',
ellipsis: true, search: {
fixed: 'left', type: 'string',
}, },
{ },
title: '名称', {
dataIndex: 'name', title: '名称',
key: 'name', dataIndex: 'name',
ellipsis: true, key: 'name',
ellipsis: true,
search: {
type: 'string',
}, },
{ },
title: '描述', {
key: 'description', title: '描述',
ellipsis: true, key: 'description',
dataIndex: 'description', ellipsis: true,
filters: true, dataIndex: 'description',
onFilter: true, search: {
type: 'string',
}, },
{ },
title: '操作', {
valueType: 'option', title: '操作',
width: 200, dataIndex: 'action',
fixed: 'right', key: 'action',
}, width: 200,
], fixed: 'right',
params: {}, scopedSlots: true,
}); },
];
const queryParams = ref({});
// //
const tableRef = ref<Record<string, any>>({}); const tableRef = ref<Record<string, any>>({});
const table = reactive({ const table = {
columns: [
{
title: '标识',
dataIndex: 'id',
key: 'id',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
],
tableData: [],
clickAdd: () => { clickAdd: () => {
addDialogRef.value.openDialog(true, {}); addDialogRef.value.openDialog(true, {});
}, },
@ -137,13 +121,11 @@ const table = reactive({
} }
}); });
}, },
clickEdit: (row: any) => { clickEdit: ({ id }: { id: string }) => {
router.push(`/system/Role/detail/${row.id}`); jumpPage(`system/Role/Detail`, { id });
}, },
}); };
nextTick(() => { const dialogVisible = ref(isSave);
route.query.save && table.clickAdd();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,14 +1,14 @@
<template> <template>
<page-container> <page-container>
<div class="user-container"> <div class="user-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="getUserList_api" :request="getUserList_api"
model="TABLE" model="TABLE"
:params="query.params.value" :params="queryParams"
:defaultParams="{ :defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }], sorts: [{ name: 'createTime', order: 'desc' }],
}" }"
@ -208,12 +208,7 @@ const columns = [
scopedSlots: true, scopedSlots: true,
}, },
]; ];
const query = { const queryParams = ({});
params: ref({}),
search: (params: object) => {
query.params.value = params;
},
};
const tableRef = ref<Record<string, any>>({}); // const tableRef = ref<Record<string, any>>({}); //
const table = { const table = {

View File

@ -3903,8 +3903,8 @@ jetlinks-store@^0.0.3:
jetlinks-ui-components@^1.0.4: jetlinks-ui-components@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.4.tgz#38f2abbb6c686ce1e1d7c08a318a6c2f50267adc" resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.4.tgz#4c7dc568b20a9bda7decbbb6b30f0191cb4340af"
integrity sha512-bpmSoKVpdgZ2FUxIBaqBpewt2T9g1tTf4tECam9uTdww6SDxyvLzWmeKIUfVIwp2Ts7uKpHPFsCtI8fPmZuRjw== integrity sha512-V1lydb150+9N+wKPaZSF+WYF1bPj1bQ8nCge8EP093tqH/wVHEs3mYRlHEEfNf/+SLIcrWuPCISihfMatVIlTQ==
dependencies: dependencies:
"@vueuse/core" "^9.12.0" "@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15" ant-design-vue "^3.2.15"