Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-02-16 09:02:16 +08:00
commit 0138941c57
24 changed files with 2711 additions and 557 deletions

View File

@ -0,0 +1,67 @@
import server from '@/utils/request'
/**
*
* @param data
* @returns
*/
export const query = (data: Record<string, any>) => server.post('/device/aliyun/bridge/_query', data)
/**
*
* @param data
* @returns
*/
export const queryProductList = (data?: Record<string, any>) => server.post('/device-product/_query/no-paging', data)
/**
*
* @param data
* @returns
*/
export const savePatch = (data: Record<string, any>) => server.patch(`/device/aliyun/bridge`, data)
/**
* ID获取阿里云详情
* @param id ID
* @returns
*/
export const detail = (id: string) => server.get(`/device/aliyun/bridge/${id}`)
/**
*
* @param id ID
* @returns
*/
export const _delete = (id: string) => server.remove(`/device/aliyun/bridge/${id}`)
/**
*
* @param id ID
* @param data
* @returns
*/
export const _deploy = (id: string) => server.post(`/device/aliyun/bridge/${id}/enable`)
/**
*
* @param id ID
* @param data
* @returns
*/
export const _undeploy = (id: string) => server.post(`/device/aliyun/bridge/${id}/disable`)
/**
*
* @param params
* @returns
*/
export const getRegionsList = (params?: Record<string, any>) => server.get(`/device/aliyun/bridge/regions`, params)
/**
*
* @param data
* @returns
*/
export const getAliyunProductsList = (data?: Record<string, any>) => server.post(`/device/aliyun/bridge/products/_query`, data)

View File

@ -6,3 +6,66 @@ import server from '@/utils/request'
* @returns
*/
export const query = (data: Record<string, any>) => server.post('/dueros/product/_query', data)
/**
*
* @param id
* @returns
*/
export const queryProductList = (id?: string) => server.post('/device-product/_query/no-paging', {
paging: false,
terms: id ? [{
column: 'id$dueros-product$not',
value: 1,
},
{ column: 'id', type: 'or', value: id }
] : [{
column: 'id$dueros-product$not',
value: 1,
}],
sorts: [{ name: 'createTime', order: 'desc' }],
})
/**
*
* @returns
*/
export const queryTypes = () => server.get('/dueros/product/types')
/**
*
* @param data dueros
* @returns
*/
export const savePatch = (data: Record<string, any>) => server.patch(`/dueros/product`, data)
/**
* duerosID获取dueros详情
* @param id duerosID
* @returns dueros详情
*/
export const detail = (id: string) => server.get(`/dueros/product/${id}`)
/**
* dueros
* @param id duerosID
* @returns
*/
export const _delete = (id: string) => server.remove(`/dueros/product/${id}`)
/**
* dueros
* @param id duerosID
* @param data
* @returns
*/
export const _deploy = (id: string) => server.post(`/dueros/product/${id}/_enable`)
/**
* dueros
* @param id duerosID
* @param data
* @returns
*/
export const _undeploy = (id: string) => server.post(`/dueros/product/${id}/_disable`)

View File

@ -0,0 +1,30 @@
import server from '@/utils/request'
/**
*
*/
export const queryList = (data: any) => server.post('/rule-engine/instance/_query', data);
/**
*
*/
export const saveRule = (data: any) => server.post('/rule-editor/flows/_create',data);
/**
*
*/
export const modify = (id:any ,data:any) => server.put(`/rule-engine/instance/${id}`,data);
/**
*
*/
export const startRule = (id:string) => server.post(`/rule-engine/instance/${id}/_start`);
/**
*
*/
export const stopRule = (id:string) => server.post(`/rule-engine/instance/${id}/_stop`);
/**
*
*/
export const deleteRule = (id:string) => server.remove(`/rule-engine/instance/${id}`)

View File

@ -1,79 +0,0 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
chartYData: {
type: Array,
default: () => [],
},
chartXData: {
type: Array,
default: () => [],
},
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
const options = {
xAxis: {
type: 'category',
data: props.chartXData,
show: false,
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '5%',
bottom: 0,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
series: [
{
name: '在线数',
data: props.chartYData.reverse(),
type: 'bar',
showBackground: true,
itemStyle: {
color: '#D3ADF7',
},
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.chartYData,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
options:{
type:Object,
default:()=>{}
}
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
myChart.setOption(props.options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.options,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,99 +0,0 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
chartYData: {
type: Array,
default: () => [],
},
chartXData: {
type: Array,
default: () => [],
},
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
const options = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
boundaryGap: false,
show: false,
data:props.chartXData
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '2%',
bottom: 0,
},
series: [
{
name: '消息量',
data: props.chartYData,
type: 'line',
smooth: true, // 线
symbolSize: 0, //
color: '#F29B55',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#FBBB87', // 100%
},
{
offset: 1,
color: '#FFFFFF', // 0%
},
],
global: false, // false
},
},
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.chartYData,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,112 +0,0 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
x: {
type: Array,
default: () => [],
},
y: {
type: Array,
default: () => [],
},
maxY:{
type:Number,
default: 0
}
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: props.x,
},
yAxis: {
type: 'value',
},
tooltip: {
trigger: 'axis',
formatter: '{b0}<br />{a0}: {c0}',
// formatter: '{b0}<br />{a0}: {c0}<br />{a1}: {c1}%'
},
grid: {
top: '2%',
bottom: '5%',
left: props.maxY > 100000 ? '90px' : '50px',
right: '50px',
},
series: [
{
name: '消息量',
data: props.y,
type: 'bar',
// type: 'line',
// smooth: true,
color: '#597EF7',
barWidth: '30%',
// areaStyle: {
// color: {
// type: 'linear',
// x: 0,
// y: 0,
// x2: 0,
// y2: 1,
// colorStops: [
// {
// offset: 0,
// color: '#685DEB', // 100%
// },
// {
// offset: 1,
// color: '#FFFFFF', // 0%
// },
// ],
// global: false, // false
// },
// },
},
{
name: '占比',
data: props.y,
// data: percentageY,
type: 'line',
smooth: true,
symbolSize: 0, //
color: '#96ECE3',
},
],
}
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.y,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -72,7 +72,7 @@ const props = defineProps({
}
.content-right {
width: 0;
height: 100%;
height: 123px;
display: flex;
flex-grow: .7;
align-items: flex-end;

View File

@ -24,10 +24,12 @@
:footer="onlineFooter"
:value="onlineToday"
>
<BarChart
<!-- <BarChart
:chartXData="barChartXData"
:chartYData="barChartYData"
></BarChart> </TopCard
></BarChart> -->
<Charts :options="onlineOptions"></Charts>
</TopCard
></a-col>
<a-col :span="6"
><TopCard
@ -35,10 +37,7 @@
:footer="messageFooter"
:value="dayMessage"
>
<LineChart
:chartXData="lineChartXData"
:chartYData="lineChartYData"
></LineChart> </TopCard
<Charts :options="TodayDevOptions"></Charts> </TopCard
></a-col>
</a-row>
<a-row :span="24">
@ -55,7 +54,7 @@
</template>
</Guide>
<div class="message-chart">
<MessageChart :x="messageChartXData" :y="messageChartYData" :maxY="messageMaxChartYData"></MessageChart>
<Charts :options="devMegOptions"></Charts>
</div>
</div>
</a-col>
@ -74,11 +73,9 @@
</page-container>
</template>
<script lang="ts" setup>
import BarChart from './components/BarChart.vue';
import LineChart from './components/LineChart.vue';
import TimeSelect from './components/TimeSelect.vue';
import Charts from './components/Charts.vue'
import Guide from './components/Guide.vue';
import MessageChart from './components/messageChart.vue';
import {
productCount,
deviceCount,
@ -130,13 +127,12 @@ let messageFooter = ref<Footer[]>([
value: 0,
},
]);
let lineChartYData = ref<any[]>([]);
let lineChartXData = ref<any[]>([]);
let barChartXData = ref<any[]>([]);
let barChartYData = ref<any[]>([]);
let messageChartXData = ref<any[]>([]);
let messageChartYData = ref<any[]>([]);
let messageMaxChartYData = ref<number>();
let onlineOptions = ref<any>({});
let TodayDevOptions = ref<any>({});
let devMegOptions = ref<any>({});
const quickBtnList = [
{ label: '昨日', value: 'yesterday' },
{ label: '近一周', value: 'week' },
@ -215,13 +211,165 @@ const getOnline = () => {
const x = res.result
.map((item: any) => item.data.timeString)
.reverse();
barChartXData.value = x;
const y = res.result.map((item: any) => item.data.value);
barChartYData.value = y;
const onlineYdata = y;
onlineYdata.reverse()
setOnlineChartOpition(x,onlineYdata);
deviceFooter.value[0].value = y?.[1];
}
});
};
const setOnlineChartOpition = (x:Array<any>,y:Array<number>):void=>{
onlineOptions.value = {
xAxis: {
type: 'category',
data: x,
show: false,
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '5%',
bottom: 0,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
series: [
{
name: '在线数',
data: y,
type: 'bar',
showBackground: true,
itemStyle: {
color: '#D3ADF7',
},
},
],
};
}
const setTodayDevChartOption = (x:Array<any>,y:Array<number>):void =>{
TodayDevOptions = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
boundaryGap: false,
show: false,
data:x
},
yAxis: {
type: 'value',
show: false,
},
grid: {
top: '2%',
bottom: 0,
},
series: [
{
name: '消息量',
data: y,
type: 'line',
smooth: true, // 线
symbolSize: 0, //
color: '#F29B55',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#FBBB87', // 100%
},
{
offset: 1,
color: '#FFFFFF', // 0%
},
],
global: false, // false
},
},
},
],
};
}
const setDevMesChartOption = (x:Array<any>,y:Array<number>,maxY:number):void =>{
devMegOptions.value = {
xAxis: {
type: 'category',
boundaryGap: false,
data: x,
},
yAxis: {
type: 'value',
},
tooltip: {
trigger: 'axis',
formatter: '{b0}<br />{a0}: {c0}',
// formatter: '{b0}<br />{a0}: {c0}<br />{a1}: {c1}%'
},
grid: {
top: '2%',
bottom: '5%',
left: maxY > 100000 ? '90px' : '50px',
right: '50px',
},
series: [
{
name: '消息量',
data: y,
type: 'bar',
// type: 'line',
// smooth: true,
color: '#597EF7',
barWidth: '30%',
// areaStyle: {
// color: {
// type: 'linear',
// x: 0,
// y: 0,
// x2: 0,
// y2: 1,
// colorStops: [
// {
// offset: 0,
// color: '#685DEB', // 100%
// },
// {
// offset: 1,
// color: '#FFFFFF', // 0%
// },
// ],
// global: false, // false
// },
// },
},
{
name: '占比',
data: y,
// data: percentageY,
type: 'line',
smooth: true,
symbolSize: 0, //
color: '#96ECE3',
},
],
}
}
getOnline();
//
const getDevice = () => {
@ -279,8 +427,7 @@ const getDevice = () => {
);
const x = today.map((item: any) => item.data.timeString).reverse();
const y = today.map((item: any) => item.data.value).reverse();
lineChartXData.value = x;
lineChartYData.value = y;
setTodayDevChartOption(x,y);
}
});
};
@ -322,15 +469,16 @@ const getEcharts = (data: any) => {
},
]).then((res:any) => {
if (res.status === 200) {
messageChartXData.value = res.result
const x = res.result
.map((item: any) =>
_time === '1h'
? `${item.data.timeString}`
: item.data.timeString,
)
.reverse();
messageChartYData.value = res.result.map((item: any) => item.data.value).reverse();
messageMaxChartYData.value = Math.max.apply(null, messageChartYData.value.length ? messageChartYData.value : [0]);
const y = res.result.map((item: any) => item.data.value).reverse();
const maxY = Math.max.apply(null, messageChartYData.value.length ? messageChartYData.value : [0]);
setDevMesChartOption(x,y,maxY);
}
});
};

View File

@ -16,9 +16,6 @@
</a-button>
</template>
</JTable>
<a-button type="link" @click="detail(slotProps)">
<AIcon type="SearchOutlined" />
</a-button>
</template>
<script lang="ts" setup>

View File

@ -281,7 +281,7 @@ const api = ref<string>('');
const type = ref<string>('');
const statusMap = new Map();
statusMap.set('online', 'processing');
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');

View File

@ -1,163 +1,170 @@
<template>
<a-card class="device-product">
<Search
:columns="query.columns"
target="product-manage"
@search="handleSearch"
/>
<JTable
:columns="columns"
:request="queryProductList"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="add"
><plus-outlined />新增</a-button
>
<a-upload
name="file"
accept=".json"
:showUploadList="false"
:before-upload="beforeUpload"
>
<a-button>导入</a-button>
</a-upload>
</a-space>
</template>
<template #deviceType="slotProps">
<div>{{ slotProps.deviceType.text }}</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3
@click.stop="handleView(slotProps.id)"
style="font-weight: 600"
<page-container>
<a-card class="device-product">
<Search
:columns="query.columns"
target="product-manage"
@search="handleSearch"
/>
<JTable
:columns="columns"
:request="queryProductList"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="add"
><plus-outlined />新增</a-button
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-upload
name="file"
accept=".json"
:showUploadList="false"
:before-upload="beforeUpload"
>
<a-button>导入</a-button>
</a-upload>
</a-space>
</template>
<template #deviceType="slotProps">
<div>{{ slotProps.deviceType.text }}</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3
@click.stop="handleView(slotProps.id)"
style="font-weight: 600"
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state === 1 ? '正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template>
<template #id="slotProps">
<a>{{ slotProps.id }}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state === 1 ? '正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template>
<template #id="slotProps">
<a>{{ slotProps.id }}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save ref="saveRef" :isAdd="isAdd" :title="title" @success="refresh" />
</a-card>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save
ref="saveRef"
:isAdd="isAdd"
:title="title"
@success="refresh"
/>
</a-card>
</page-container>
</template>
<script setup lang="ts">
@ -195,7 +202,7 @@ import Save from './Save/index.vue';
const router = useRouter();
const isAdd = ref<number>(0);
const title = ref<string>('');
const params = <Record<string, any>>{};
const params = ref<Record<string, any>>({});
const statusMap = new Map();
statusMap.set(1, 'success');
statusMap.set(0, 'error');

View File

@ -0,0 +1,94 @@
<template>
<div class="doc">
<div className="url">
阿里云物联网平台
<a
:style="{ wordBreak: 'break-all' }"
href="https://help.aliyun.com/document_detail/87368.html"
target="_blank"
rel="noreferrer"
>
https://help.aliyun.com/document_detail/87368.html
</a>
</div>
<h1>1. 概述</h1>
<div>
在特定场景下设备无法直接接入阿里云物联网平台时您可先将设备接入物联网平台再使用阿里云云云对接SDK快速构建桥接服务搭建物联网平台与阿里云物联网平台的双向数据通道
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun2.png')" />
</div>
<h1>2.配置说明</h1>
<div>
<h2> 1服务地址</h2>
<div>
阿里云内部给每台机器设置的唯一编号请根据购买的阿里云服务器地址进行选择
</div>
<div>获取路径阿里云物联网平台--服务地址</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun3.png')" />
</div>
<h2> 2AccesskeyID/Secret</h2>
<div>
用于程序通知方式调用云服务费API的用户标识和秘钥获取路径阿里云管理控制台--用户头像----AccessKey管理--查看
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun1.jpg')" />
</div>
<h2> 3. 网桥产品</h2>
<div>
物联网平台对于阿里云物联网平台是一个网关设备需要映射到阿里云物联网平台的具体产品
</div>
<h2> 4. 产品映射</h2>
<div>
将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联关联后需要进入该产品下的每一个设备的实例信息页填入对应的阿里云物联网平台设备的DeviceNameDeviceSecret进行一对一绑定
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun4.png')" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
</script>
<style lang="less" scoped>
.doc {
height: 1000px;
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}
</style>

View File

@ -0,0 +1,333 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="16">
<TitleComponent data="基本信息" />
<a-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="名称" name="name" :rules=" [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]">
<a-input placeholder="请输入名称" v-model:value="modelRef.name" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :name="['accessConfig', 'regionId']" :rules="[{
required: true,
message: '请选择服务地址',
}]">
<template #label>
<span>
服务地址
<a-tooltip title="阿里云内部给每台机器设置的唯一编号">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-select placeholder="请选择服务地址" v-model:value="modelRef.accessConfig.regionId" show-search :filter-option="filterOption" @blur="productChange">
<a-select-option v-for="item in regionsList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :name="['accessConfig', 'instanceId']">
<template #label>
<span>
实例ID
<a-tooltip title="阿里云物联网平台中的实例ID,没有则不填">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-input placeholder="请输入实例ID" v-model:value="modelRef.accessConfig.instanceId" @blur="productChange" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :name="['accessConfig', 'accessKeyId']" :rules="[{
required: true,
message: '请输入accessKey',
},
{
max: 64,
message: '最多输入64个字符',
},
]">
<template #label>
<span>
accessKey
<a-tooltip title="用于程序通知方式调用云服务API的用户标识">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-input placeholder="请输入accessKey" v-model:value="modelRef.accessConfig.accessKeyId" @blur="productChange" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :name="['accessConfig', 'accessSecret']" :rules="[{
required: true,
message: '请输入accessSecret',
},
{
max: 64,
message: '最多输入64个字符',
},
]">
<template #label>
<span>
accessSecret
<a-tooltip title="用于程序通知方式调用云服务费API的秘钥标识">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-input placeholder="请输入accessSecret" v-model:value="modelRef.accessConfig.accessSecret" @blur="productChange" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item name="bridgeProductKey" :rules="{
required: true,
message: '请选择网桥产品',
}">
<template #label>
<span>
网桥产品
<a-tooltip title="物联网平台对应的阿里云产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-select placeholder="请选择网桥产品" v-model:value="modelRef.bridgeProductKey" show-search :filter-option="filterOption">
<a-select-option v-for="item in aliyunProductList" :key="item.productKey" :value="item.productKey" :label="item.productName">{{item.productName}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
<p>产品映射</p>
<a-collapse v-if="modelRef.mappings.length" :activeKey="modelRef.mappings.map((_, _index) => _index)">
<a-collapse-panel v-for="(item, index) in modelRef.mappings" :key="index" :header="item.productKey ? aliyunProductList.find(i => i.productKey === item.productKey)?.productName : `产品映射${index + 1}`">
<template #extra><AIcon type="DeleteOutlined" @click="delItem(index)" /></template>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="阿里云产品" :name="['mappings', index, 'productKey']" :rules="{
required: true,
message: '请选择阿里云产品',
}">
<a-select placeholder="请选择阿里云产品" v-model:value="item.productKey" show-search :filter-option="filterOption">
<a-select-option v-for="i in getAliyunProductList(item.productKey)" :key="i.productKey" :value="i.productKey" :label="i.productName">{{i.productName}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="平台产品" :name="['mappings', index, 'productId']" :rules="{
required: true,
message: '请选择平台产品',
}">
<a-select placeholder="请选择平台产品" v-model:value="item.productId" show-search :filter-option="filterOption">
<a-select-option v-for="i in getPlatProduct(item.productId)" :key="i.id" :value="item.id" :label="i.name">{{i.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col :span="24">
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addItem">
<AIcon
type="PlusOutlined"
style="margin-left: 2px;" />添加
</a-button>
</a-col>
<a-col :span="24" style="margin-top: 20px">
<a-form-item label="说明" name="description" :rules="{
max: 200,
message: '最多输入200个字符',
}">
<a-textarea
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div v-if="type === 'edit'">
<a-button :loading="loading" type="primary" @click="saveBtn">保存</a-button>
</div>
</a-col>
<a-col :span="8">
<Doc />
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import Doc from './doc.vue'
import {savePatch, detail, getRegionsList, getAliyunProductsList, queryProductList } from '@/api/northbound/alicloud'
import _ from 'lodash';
import { message } from 'ant-design-vue';
const router = useRouter();
const route = useRoute();
const formRef = ref();
const modelRef = reactive({
id: undefined,
name: undefined,
accessConfig: {
regionId: undefined,
instanceId: undefined,
accessKeyId: undefined,
accessSecret: undefined
},
bridgeProductKey: undefined,
bridgeProductName: undefined,
mappings: [{
productKey: undefined,
productId: undefined,
}],
description: undefined
});
const addItem = () => {
modelRef.mappings.push({
productKey: undefined,
productId: undefined,
})
}
const delItem = (index: number) => {
modelRef.mappings.splice(index, 1)
}
const productList = ref<Record<string, any>[]>([])
const regionsList = ref<Record<string, any>[]>([])
const aliyunProductList = ref<Record<string, any>[]>([])
const loading = ref<boolean>(false)
const type = ref<'edit' | 'view'>('view')
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const queryRegionsList = async () => {
const resp = await getRegionsList()
if(resp.status === 200){
regionsList.value = resp.result as Record<string, any>[]
}
}
const getProduct = async () => {
const resp = await queryProductList({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
})
if(resp.status === 200){
productList.value = (resp?.result as Record<string, any>[])
}
}
const getAliyunProduct = async (data: any) => {
if(data.regionId && data.accessKeyId && data.accessSecret){
const resp: any = await getAliyunProductsList(data)
if(resp.status === 200){
aliyunProductList.value = (resp?.result?.data as Record<string, any>[])
}
}
}
const productChange = () => {
const data = modelRef.accessConfig
getAliyunProduct(data)
}
const getPlatProduct = (val: string) => {
const arr = modelRef.mappings.map(item => item?.productId) || []
const checked = _.cloneDeep(arr)
const _index = checked.findIndex(i => i === val)
checked.splice(_index, 1)
const list = productList.value.filter((i: any) => !checked.includes(i?.id as any))
return list || []
}
const getAliyunProductList = (val: string) => {
const items = modelRef.mappings.map((item) => item?.productKey) || []
const checked = _.cloneDeep(items)
const _index = checked.findIndex(i => i === val)
checked.splice(_index, 1)
const list = aliyunProductList.value?.filter((i: any) => !checked.includes(i?.productKey as any))
return list || []
}
const saveBtn = async () => {
const data = await formRef.value.validate()
const product = (aliyunProductList.value || []).find(
(item: any) => item?.bridgeProductKey === data?.bridgeProductKey,
);
data.bridgeProductName = product?.productName || '';
loading.value = true;
const resp = await savePatch(toRaw(modelRef));
loading.value = false;
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();
router.push('/iot/northbound/AliCloud/');
}
}
watch(
() => route.params?.id,
async (newId) => {
if(newId){
queryRegionsList()
getProduct()
if (newId === ':id' || !newId) return;
const resp = await detail(newId as string)
const _data: any = resp.result;
if (_data) {
getAliyunProduct(_data?.accessConfig)
}
Object.assign(modelRef, _data)
}
},
{immediate: true, deep: true}
);
watch(
() => route.query.type,
(newVal) => {
if(newVal){
type.value = newVal as 'edit' | 'view'
}
},
{immediate: true, deep: true}
);
</script>

View File

@ -1,7 +1,306 @@
<template>
<page-container>阿里云</page-container>
<page-container>
<Search :columns="columns" target="northbound-dueros" :params="params" />
<JTable
ref="instanceRef"
:columns="columns"
:request="query"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="handleAdd">新增</a-button>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error'
}"
>
<template #img>
<slot name="img">
<img
:src="
getImage('/northbound/aliyun.png')
"
/>
</slot>
</template>
<template #content>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
网桥产品
</div>
<div>{{ slotProps?.bridgeProductName }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
<label>说明</label>
</div>
<div>{{ slotProps?.description }}</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
</page-container>
</template>
<script setup>
<script setup lang="ts">
import {
query,
_undeploy,
_deploy,
_delete
} from '@/api/northbound/alicloud';
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
const router = useRouter();
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const current = ref<Record<string, any>>({});
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '网桥产品',
dataIndex: 'bridgeProductName',
key: 'bridgeProductName',
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
/**
* 新增
*/
const handleAdd = () => {
router.push('/iot/northbound/AliCloud/detail/:id');
};
/**
* 查看
*/
const handleView = (id: string) => {
router.push({
path: '/iot/northbound/AliCloud/detail/' + id,
query: {
type: 'view'
}
});
};
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id);
},
},
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
router.push({
path: '/iot/northbound/AliCloud/detail/' + data.id,
query: {
type: 'edit'
}
});
},
},
{
key: 'action',
text: data.state?.value !== 'disabled' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disabled' ? '禁用' : '启用',
},
icon:
data.state.value !== 'notActive'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disabled') {
response = await _undeploy(data.id);
} else {
response = await _deploy(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
instanceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'disabled',
tooltip: {
title:
data.state.value !== 'disabled'
? '请先禁用该数据,再删除。'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
instanceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
</script>

View File

@ -0,0 +1,93 @@
<template>
<a-table
rowKey="id"
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<div style="width: 280px">
<template v-if="['valueType', 'name'].includes(column.dataIndex)">
<span>{{ text }}</span>
</template>
<template v-else>
<ValueItem
v-model:modelValue="record.value"
:itemType="record.type"
:options="
record.type === 'enum'
? (record?.dataType?.elements || []).map(
(item) => {
return {
label: item.text,
value: item.value,
};
},
)
: record.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</template>
</div>
</template>
</a-table>
</template>
<script lang="ts" setup>
import { PropType } from "vue-demi";
type Emits = {
(e: 'update:modelValue', data: Record<string, any>[]): void;
};
const _emit = defineEmits<Emits>();
const _props = defineProps({
modelValue: {
type: Array as PropType<Record<string, any>[]>,
default: '',
}
});
const columns = [
{
title: '参数名称',
dataIndex: 'name',
with: '33%',
},
{
title: '类型',
dataIndex: 'valueType',
with: '33%',
},
{
title: '值',
dataIndex: 'value',
with: '34%',
},
];
// const dataSource = ref<Record<any, any>[]>(_props.modelValue || []);
const dataSource = computed({
get: () => {
return _props.modelValue || {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
}
}
},
set: (val: any) => {
_emit('update:modelValue', val);
}
})
</script>

View File

@ -0,0 +1,139 @@
<template>
<a-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<a-row :gutter="24">
<a-col :span="24" v-if="actionType === 'command'">
<a-form-item name="messageType" label="指令类型" :rules="{
required: true,
message: '请选择指令类型',
}">
<a-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption">
<a-select-option value="READ_PROPERTY">读取属性</a-select-option>
<a-select-option value="WRITE_PROPERTY">修改属性</a-select-option>
<a-select-option value="INVOKE_FUNCTION">调用功能</a-select-option>
</a-select>
</a-form-item>
</a-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'">
<a-form-item :name="['message', 'properties']" label="属性" :rules="{
required: true,
message: '请选择属性',
}">
<a-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>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12" v-if="modelRef.messageType === 'WRITE_PROPERTY' && actionType === 'command'">
<a-form-item :name="['message', 'value']" label="值" :rules="{
required: true,
message: '请输入值',
}">
<a-input />
</a-form-item>
</a-col>
<a-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
<a-form-item :name="['message', 'functionId']" label="功能" :rules="{
required: true,
message: '请选择功能',
}">
<a-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>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION' && modelRef.message.functionId">
<a-form-item :name="['message', 'inputs']" label="参数列表" :rules="{
required: true,
message: '请输入参数列表',
}">
<EditTable v-model="modelRef.message.inputs"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import EditTable from './EditTable.vue'
const formRef = ref();
const funcList = ref<Record<string, any>[]>([])
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const props = defineProps({
actionType: {
type: String,
default: ''
},
modelValue: {
type: Object,
default: () => {}
},
metadata: {
type: Object,
default: () => {
return {
properties: [],
functions: []
}
}
}
})
type Emits = {
(e: 'update:modelValue', data: any): void;
};
const emit = defineEmits<Emits>();
const modelRef = computed({
get: () => {
return props.modelValue || {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
}
}
},
set: (val: any) => {
emit('update:modelValue', val);
}
})
const funcChange = (val: string) => {
if(val){
const arr = props.metadata?.functions.find((item: any) => item.id === val)?.inputs || []
const list = arr.map((item: any) => {
return {
id: item.id,
name: item.name,
value: undefined,
valueType: item?.valueType?.type,
}
})
modelRef.value.message.inputs = list
}
}
const saveBtn = () => new Promise((resolve) => {
formRef.value.validate()
.then(() => {
resolve(toRaw(modelRef))
})
.catch((err: any) => {
resolve(false)
});
})
defineExpose({ saveBtn })
</script>

View File

@ -0,0 +1,101 @@
<template>
<div class="doc">
<div class="url">
小度智能家居开放平台
<a
href="https://dueros.baidu.com/dbp/bot/index#/iotopenplatform"
target="_blank"
rel="noopener noreferrer"
>
https://dueros.baidu.com/dbp/bot/index#/iotopenplatform
</a>
</div>
<h1>1. 概述</h1>
<div>
DuerOS支持家居场景下的云端控制该页面主要将平台的产品与DuerOS支持语音控制的产品进行映射以到达小度平台控制本平台设备的目的
</div>
<h1>2. 操作步骤</h1>
<div>
<h2>1在百度小度技能平台创建技能并授权完成物联网平台与DuerOS的关联</h2>
<div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc.jpg')" />
</div>
<h1>授权地址</h1>
<div>物联网平台的登录地址注意需要为https</div>
<div>请复制并填写: https://{location.host}/#/user/login</div>
<h1>Client_Id</h1>
<div>请填写系统管理-应用管理中的clientId</div>
<div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc1.png')" />
</div>
<h1>回调地址</h1>
<div>请复制DuerOS平台中的值填写到系统管理-应用管理中-redirectUrl中</div>
<div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc2.png')" />
</div>
<h1>Token地址</h1>
<div>请复制并填写HTTPS://{location.host}/api/v1/token</div>
<h1>ClientSecret</h1>
<div>请复制系统管理-应用管理中的secureKey填写到DuerOS平台</div>
<div class="image">
<a-image width="100%" :src="getImage('/cloud/dueros-doc3.png')" />
</div>
<div></div>
<h1>WebService</h1>
<div>请复制并填写/dueros/product/_query</div>
<h2>2登录物联网平台进行平台内产品与DuerOS产品的数据映射</h2>
<h2>
3智能家居用户通过物联网平台中的用户登录小度APP获取平台内当前用户的所属设备获取后即可进行语音控制
</h2>
</div>
<h1>3. 配置说明</h1>
<div>
<h2>
1设备类型为DuerOS平台拟定的标准规范设备类型将决定动作映射动作的下拉选项以及属性映射Dueros属性的下拉选项
</h2>
</div>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
</script>
<style lang="less" scoped>
.doc {
height: 1000px;
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}
</style>

View File

@ -0,0 +1,385 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="16">
<TitleComponent data="基本信息" />
<a-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="名称" name="name" :rules=" [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]">
<a-input placeholder="请输入名称" v-model:value="modelRef.name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="产品" name="id" :rules="[{
required: true,
message: '请选择产品',
}]">
<a-select :disabled="modelRef.id !== ':id'" placeholder="请选择产品" v-model:value="modelRef.id" show-search :filter-option="filterOption" @change="productChange">
<a-select-option v-for="item in productList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item name="applianceType" :rules="{
required: true,
message: '请选择设备类型',
}">
<template #label>
<span>
设备类型
<a-tooltip title="DuerOS平台拟定的规范">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-select placeholder="请选择设备类型" v-model:value="modelRef.applianceType" show-search :filter-option="filterOption" @change="typeChange">
<a-select-option v-for="item in typeList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="productName" v-show="false" label="产品名称">
<a-input v-model:value="modelRef.productName" />
</a-form-item>
</a-col>
<a-col :span="24">
<p>动作映射</p>
<a-collapse v-if="modelRef.actionMappings.length" :activeKey="modelRef.actionMappings.map((_, _index) => _index)">
<a-collapse-panel v-for="(item, index) in modelRef.actionMappings" :key="index" :header="item.action ? getTypesActions(item.action).find(i => i.id === item.action)?.name : `动作映射${index + 1}`">
<template #extra><AIcon type="DeleteOutlined" @click="delItem(index)" /></template>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item :name="['actionMappings', index, 'action']" :rules="{
required: true,
message: '请选择动作',
}">
<template #label>
<span>
动作
<a-tooltip title="DuerOS平台拟定的设备类型具有的相关动作">
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</span>
</template>
<a-select placeholder="请选择动作" v-model:value="item.action" show-search :filter-option="filterOption">
<a-select-option v-for="i in getTypesActions(item.action)" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :name="['actionMappings', index, 'actionType']" :rules="{
required: true,
message: '请选择操作',
}">
<template #label>
<span>
操作
<a-tooltip title="映射物联网平台中所选产品具备的动作">
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</span>
</template>
<a-select placeholder="请选择操作" v-model:value="item.actionType" show-search :filter-option="filterOption">
<a-select-option value="command">下发指令</a-select-option>
<a-select-option value="latestData">获取历史数据</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24" v-if="item.actionType">
<a-form-item :name="['actionMappings', index, 'command']">
<Command ref="command" :metadata="findProductMetadata" v-model:modelValue="item.command" :actionType="item.actionType" />
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col :span="24">
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addItem">
<AIcon
type="PlusOutlined"
style="margin-left: 2px;" />新增动作
</a-button>
</a-col>
<a-col :span="24">
<p style="margin-top: 20px">属性映射</p>
<a-collapse v-if="modelRef.propertyMappings.length" :activeKey="modelRef.propertyMappings.map((_, _index) => _index)">
<a-collapse-panel v-for="(item, index) in modelRef.propertyMappings" :key="index" :header="item.source ? getDuerOSProperties(item.source).find(i => i.id === item.source)?.name : `属性映射${index + 1}`">
<template #extra><AIcon type="DeleteOutlined" @click="delPropertyItem(index)" /></template>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="DuerOS属性" :name="['propertyMappings', index, 'source']" :rules="{
required: true,
message: '请选择DuerOS属性',
}">
<a-select placeholder="请选择DuerOS属性" v-model:value="item.source" show-search :filter-option="filterOption">
<a-select-option v-for="i in getDuerOSProperties(item.source)" :key="i.id" :value="i.id">{{i.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="平台属性" :name="['propertyMappings', index, 'target']" :rules="{
required: true,
message: '请选择平台属性',
}">
<a-select placeholder="请选择平台属性" v-model:value="item.target" mode="tags" show-search :filter-option="filterOption">
<a-select-option v-for="i in getProductProperties(item.target)" :key="i.id" :value="item.id">{{i.name}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col :span="24">
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addPropertyItem">
<AIcon
type="PlusOutlined"
style="margin-left: 2px;" />新增属性
</a-button>
</a-col>
<a-col :span="24" style="margin-top: 20px">
<a-form-item label="说明" name="description" :rules="{
max: 200,
message: '最多输入200个字符',
}">
<a-textarea
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div v-if="type === 'edit'">
<a-button :loading="loading" type="primary" @click="saveBtn">保存</a-button>
</div>
</a-col>
<a-col :span="8">
<Doc />
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import Doc from './doc.vue'
import Command from './command/index.vue'
import { queryProductList, queryTypes, savePatch, detail } from '@/api/northbound/dueros'
import _ from 'lodash';
import { message } from 'ant-design-vue';
const router = useRouter();
const route = useRoute();
const formRef = ref();
const modelRef = reactive({
id: undefined,
name: undefined,
applianceType: undefined,
productName: undefined,
actionMappings: [{
actionType: undefined,
action: undefined,
command: {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
}
}
}],
propertyMappings: [{
source: undefined,
target: []
}],
description: undefined
});
const addItem = () => {
modelRef.actionMappings.push({
actionType: undefined,
action: undefined,
command: {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
}
}
})
}
const productList = ref<Record<string, any>[]>([])
const typeList = ref<Record<string, any>[]>([])
const command = ref([])
const loading = ref<boolean>(false)
const type = ref<'edit' | 'view'>('view')
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const delItem = (index: number) => {
modelRef.actionMappings.splice(index, 1)
}
const addPropertyItem = () => {
modelRef.propertyMappings.push({
source: undefined,
target: []
})
}
const delPropertyItem = (index: number) => {
modelRef.propertyMappings.splice(index, 1)
}
const productChange = (value: string) => {
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
return {source: item.source, target: []}
})
const item = productList.value.find(item => item.id === value)
if(item){
modelRef.productName = item.name
}
}
const typeChange = () => {
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
return {source: undefined, target: item.target}
})
modelRef.actionMappings = modelRef.actionMappings.map(item => {
return {...item, action: undefined}
})
}
const findApplianceType = computed(() => {
if(!modelRef.applianceType) return
return typeList.value.find(item => item.id === modelRef.applianceType)
})
const findProductMetadata = computed(() => {
if(!modelRef.id) return
const _product = productList.value?.find((item: any) => item.id === modelRef.id)
return _product?.metadata && JSON.parse(_product.metadata || '{}')
})
//
const getProduct = async (id?: string) => {
const resp = await queryProductList(id)
if(resp.status === 200){
productList.value = (resp?.result as Record<string, any>[])
}
}
const getTypes = async () => {
const resp = await queryTypes()
if(resp.status === 200){
typeList.value = (resp?.result as Record<string, any>[])
}
}
const getDuerOSProperties = (val: string) => {
const arr = modelRef.propertyMappings.map(item => item?.source) || []
const checked = _.cloneDeep(arr)
const _index = checked.findIndex(i => i === val)
//
checked.splice(_index, 1)
const targetList = findApplianceType.value?.properties;
const list = targetList?.filter((i: {id: string}) => !checked.includes(i?.id as any))
return list || []
}
const getProductProperties = (val: string[]) => {
const items = modelRef.propertyMappings.map((item: {target: string[]}) => item?.target.map(j => j)) || []
const checked = _.flatMap(items)
const _checked: any[] = []
checked.map(_item => {
if(!val.includes(_item)){
_checked.push(_item)
}
})
const sourceList = findProductMetadata.value?.properties
const list = sourceList?.filter((i: { id: string }) => !_checked.includes(i.id))
return list || []
}
const getTypesActions = (val: string) => {
const items = modelRef.actionMappings.map((item) => item?.action) || []
const checked = _.cloneDeep(items)
const _index = checked.findIndex(i => i === val)
checked.splice(_index, 1)
const actionsList = findApplianceType.value?.actions || []
const list = actionsList?.filter((i: { id: string, name: string }) => !checked.includes(i?.id as any))
return list || []
}
const saveBtn = async () => {
const tasks = []
for(let i = 0; i < command.value.length; i++){
const res = await (command.value[i] as any)?.saveBtn()
tasks.push(res)
if(!res) break
}
const data = await formRef.value.validate()
if(tasks.every(item => item) && data){
loading.value = true;
const resp = await savePatch(toRaw(modelRef));
loading.value = false;
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();
router.push('/iot/northbound/DuerOS/');
}
}
}
watch(
() => route.params?.id,
async (newId) => {
if(newId){
getProduct(newId as string)
getTypes()
if (newId === ':id') return;
const resp = await detail(newId as string)
const _data: any = resp.result;
if (_data) {
_data.applianceType = _data?.applianceType?.value;
}
Object.assign(modelRef, _data)
}
},
{immediate: true, deep: true}
);
watch(
() => route.query.type,
(newVal) => {
if(newVal){
type.value = newVal as 'edit' | 'view'
}
},
{immediate: true, deep: true}
);
</script>

View File

@ -1,3 +0,0 @@
<template>
123
</template>

View File

@ -1,69 +1,141 @@
<template>
<page-container>
<Search :columns="columns" target="northbound-dueros" :params="params" />
<JTable
ref="instanceRef"
:columns="columns"
:request="request"
:request="query"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-button type="primary" @click="add">新增</a-button>
<a-space>
<a-button type="primary" @click="handleAdd">新增</a-button>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps)"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state ? 'success' : 'error'"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error'
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
<img
:src="
getImage('/cloud/dueros.png')
"
/>
</slot>
</template>
<template #content>
<h3>{{slotProps.name}}</h3>
<a-row>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
产品
</div>
<div>{{ slotProps?.productName }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
<div>{{ slotProps?.applianceType?.text }}</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</template>
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #id="slotProps">
<a>{{slotProps.id}}</a>
<template #state="slotProps">
<a-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #applianceType="slotProps">
{{slotProps.applianceType.text}}
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip v-for="i in getActions(slotProps)" :key="i.key" v-bind="i.tooltip">
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button :disabled="i.disabled" style="padding: 0" type="link"><AIcon :type="i.icon" /></a-button>
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button style="padding: 0" type="link" v-else @click="i.onClick && i.onClick(slotProps)">
<a-button :disabled="i.disabled" style="padding: 0" type="link"><AIcon :type="i.icon" /></a-button>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
@ -73,13 +145,24 @@
</template>
<script setup lang="ts">
import { query } from '@/api/northbound/dueros'
import type { ActionsType } from '@/components/Table/index.vue'
import {
query,
_undeploy,
_deploy,
_delete
} from '@/api/northbound/dueros';
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { DeleteOutlined } from '@ant-design/icons-vue'
import { message } from "ant-design-vue";
import { message } from 'ant-design-vue';
const request = (data: any) => query({})
const router = useRouter();
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const current = ref<Record<string, any>>({});
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const columns = [
{
@ -88,67 +171,146 @@ const columns = [
key: 'name',
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
scopedSlots: true
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '分类',
dataIndex: 'classifiedName',
key: 'classifiedName',
title: '设备类型',
dataIndex: 'applianceType',
key: 'applianceType',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true
}
]
scopedSlots: true,
},
];
const handleClick = (dt: any) => {
/**
* 新增
*/
const handleAdd = () => {
router.push('/iot/northbound/DuerOS/detail/:id');
};
}
/**
* 查看
*/
const handleView = (id: string) => {
// router.push('/iot/northbound/DuerOS/detail/' + id);
router.push({
path: '/iot/northbound/DuerOS/detail/' + id,
query: {
type: 'view'
}
});
};
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if(!data){
return []
}
return [
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'edit',
text: "编辑",
key: 'view',
text: '查看',
tooltip: {
title: '编辑'
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id);
},
icon: 'icon-rizhifuwu'
},
{
key: 'import',
text: "导入",
key: 'edit',
text: '编辑',
tooltip: {
title: '导入'
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
router.push({
path: '/iot/northbound/DuerOS/detail/' + data.id,
query: {
type: 'edit'
}
});
},
},
{
key: 'action',
text: data.state?.value !== 'disabled' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disabled' ? '禁用' : '启用',
},
icon:
data.state.value !== 'notActive'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disabled') {
response = await _undeploy(data.id);
} else {
response = await _deploy(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
instanceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
disabled: true,
icon: 'icon-xiazai'
},
{
key: 'delete',
text: "删除",
text: '删除',
disabled: data.state?.value !== 'disabled',
tooltip: {
title: !!data?.state ? '正常的产品不能删除' : '删除'
title:
data.state.value !== 'disabled'
? '请先禁用该数据,再删除。'
: '删除',
},
popConfirm: {
title: '确认删除?'
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
instanceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'icon-huishouzhan'
}
]
}
const add = () => {
// router.push(`/northbound/DuerOS/detail/:id`)
}
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
</script>

View File

@ -0,0 +1,138 @@
<template>
<a-modal
:maskClosable="false"
width="650px"
destroyOnClose
v-model:visible="visible"
:title="props.title"
@ok="handleSave"
@cancel="handleCancel"
okText="确定"
cancelText="取消"
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<a-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="modelRef"
>
<a-form-item label="名称" name="name">
<a-input
v-model:value="modelRef.name"
placeholder="请输入名称"
/>
</a-form-item>
<a-form-item label="说明" name="describe">
<a-textarea
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
:rows="4"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { saveRule , modify } from '@/api/rule-engine/instance';
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
const emit = defineEmits(['success']);
const props = defineProps({
title: {
type: String,
default: '',
},
isAdd: {
type: Number,
default: '',
},
});
const productList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const visible = ref<boolean>(false);
const formRef = ref();
let id = ref<string>();
const modelRef = reactive({
name: '',
description: '',
});
const rules = {
name: [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
],
};
watch(
() => props.isAdd,
() => {},
{ immediate: true, deep: true },
);
const handleCancel = () => {
visible.value = false;
};
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
if (props.isAdd === 1) {
let resp = await saveRule(modelRef);
loading.value = false;
if (resp.status === 200) {
message.success('操作成功!');
emit('success');
formRef.value.resetFields();
visible.value = false;
}else{
message.error('操作失败')
}
}else if(props.isAdd === 2) {
let resp = await modify(id,modelRef);
loading.value = false;
if (resp.status === 200) {
message.success('操作成功!');
emit('success');
formRef.value.resetFields();
visible.value = false;
}else{
message.error('操作失败!');
}
}
})
.catch((err: any) => {
console.log('error', err);
});
};
const show = (data: any) => {
if (props.isAdd === 1) {
modelRef.name = '';
modelRef.description = '';
} else if (props.isAdd === 2) {
modelRef.name = data?.name;
modelRef.description = data?.description;
id = data.id
}
visible.value = true;
};
defineExpose({
show: show,
});
</script>

View File

@ -0,0 +1,333 @@
<template>
<page-container>
<a-card>
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
<JTable
:columns="columns"
:request="queryList"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="add"
><plus-outlined/>新增</a-button
>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
started: 'success',
disable: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3 style="font-weight: 600">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="rule-desc">
{{ slotProps.description }}
</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="
slotProps.state?.value === 'started'
? '正常'
: '禁用'
"
:status="
slotProps.state?.value === 'started'
? 'success'
: 'error'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save
ref="saveRef"
:isAdd="isAdd"
:title="title"
@success="refresh"
/>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import JTable from '@/components/Table';
import type { InstanceItem } from './typings';
import { queryList , startRule , stopRule , deleteRule} from '@/api/rule-engine/instance';
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
import Save from './Save/index.vue';
const params = ref<Record<string, any>>({});
let isAdd = ref<number>(0);
let title = ref<string>('');
let saveRef = ref();
let currentForm = ref();
const tableRef = ref<Record<string, any>>({});
const query = {
columns: [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
search: {
type: 'select',
options: [
{
label: '正常',
value: 'started',
},
{
label: '禁用',
value: 'disable',
},
],
},
},
],
};
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type?: 'card' | 'table',
): ActionsType[] => {
if (!data) {
return [];
}
const actions = [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
title.value = '编辑';
isAdd.value = 2;
nextTick(() => {
saveRef.value.show(data);
});
},
},
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
},
{
key: 'action',
text: data.state?.value !== 'disable' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disable' ? '禁用' : '启用',
},
icon: data.state?.value !== 'disable' ? 'StopOutlined' : 'CheckCircleOutlined',
popConfirm: {
title: `确认${data.state !== 'disable' ? '禁用' : '启用'}?`,
onConfirm: async () => {
let response = undefined;
if (data.state?.value !== 'started') {
response = await startRule(data.id);
} else {
response = await stopRule(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data?.state?.value !== 'disable',
tooltip: {
title:
data?.state?.value !== 'disable'
? '请先禁用再删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await deleteRule(data.id);
if (resp.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const add = () => {
isAdd.value = 1;
title.value = '新增';
nextTick(() => {
saveRef.value.show(currentForm.value);
});
};
/**
* 刷新数据
*/
const refresh = () => {
tableRef.value?.reload();
};
const handleSearch = (e: any) => {
console.log(e);
params.value = e;
};
</script>
<style scoped>
.rule-desc {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</style>

View File

@ -0,0 +1,15 @@
type InstanceItem = {
createTime: number;
modelId: string;
modelMeta: string;
modelType: string;
modelVersion: number;
description?: string;
state: {
text: string;
value: string;
};
} & {
id:string,
name:string
}