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

This commit is contained in:
JiangQiming 2023-02-16 13:45:52 +08:00
commit b55a7bd490
49 changed files with 6078 additions and 652 deletions

32
src/api/link/type.ts Normal file
View File

@ -0,0 +1,32 @@
import server from '@/utils/request';
export const queryNetworkConfig = (params: object) =>
server.post(`/network/config/_query`, params);
export const remove = (id: string) => server.remove(`/network/config/${id}`);
export const shutdown = (data: object) =>
server.post(`/network/config/${data}/_shutdown`);
export const start = (data: object) =>
server.post(`/network/config/${data}/_start`);
export const supports = () => server.get(`/network/config/supports`);
export const query = (data: Object) =>
server.post(`/network/config/_query`, data);
export const providers = () => server.get(`/gateway/device/providers`);
export const resourcesCurrent = () =>
server.get(`/network/resources/alive/_current`);
export const resourceClusters = () => server.get(`network/resources/clusters`);
export const resourceClustersById = (id: string) =>
server.get(`/network/resources/alive/${id}`);
export const allResources = () => server.get(`/network/resources/alive/_all`);
export const certificates = () =>
server.get(`/network/certificate/_query/no-paging?paging=false`);

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

@ -10,12 +10,14 @@ export const updateDepartment_api = (data: object) => server.patch(`/organizatio
export const delDepartment_api = (id: string) => server.remove(`/organization/${id}`);
// 获取所属产品列表
export const getDeviceProduct_api = (data: object) => server.get(`/device/product/_query/no-paging`, data);
// 获取产品列表
export const getDeviceOrProductList_api = (data: object) => server.post(`/device-product/_query`, data);
// 获取设备列表
export const getDeviceList_api = (data: object) => server.post(`/device/instance/_query`, data);
// 根据产品的id获取产品的权限
export const getPermission_api = (type:'device' | 'product',ids: object, id: string) => server.post(`/assets/bindings/${type}/org/${id}/_query`, ids);
export const getPermission_api = (type: 'device' | 'product', ids: object, id: string) => server.post(`/assets/bindings/${type}/org/${id}/_query`, ids);
// 获取产品的权限字典
export const getPermissionDict_api = () => server.get(`/assets/bindings/product/permissions`);
@ -25,3 +27,12 @@ export const bindDeviceOrProductList_api = (type: 'device' | 'product', data: ob
export const unBindDeviceOrProduct_api = (type: 'device' | 'product', data: object) => server.post(`/assets/unbind/${type}`, data);
// 批量更新权限
export const updatePermission_api = (type: 'device' | 'product', parentId: string, data: object) => server.put(`/assets/permission/${type}/org/${parentId}/_batch`, data);
// 用户相关
// 获取绑定用户列表
export const getBindUserList_api = (data: object) => server.post(`/user/_query`, data);
// 绑定用户
export const bindUser_api = (parentId:string,data: object) => server.post(`/organization/${parentId}/users/_bind`, data);
// 解绑用户
export const unBindUser_api = (parentId:string,data: object) => server.post(`/organization/${parentId}/users/_unbind`, data);

36
src/api/system/user.ts Normal file
View File

@ -0,0 +1,36 @@
import server from '@/utils/request';
// 获取用户类型
export const getUserType_api = () => server.get(`/user/detail/types`);
// 获取用户列表
export const getUserList_api = (data: object) => server.post(`/user/detail/_query`, data);
// 校验字段合法性
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name, {
headers: {
'Content-Type': 'text/plain'
}
})
// 获取角色列表
export const getRoleList_api = () => server.get(`/role/_query/no-paging?paging=false`);
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree?paging=false`);
// 获取用户信息
export const getUser_api = (id: string) => server.get(`/user/detail/${id}`);
// 添加用户
export const addUser_api = (data: object) => server.post(`/user/detail/_create`, data);
// 更新用户
export const updateUser_api = (data: any) => server.put(`/user/detail/${data.id}/_update`, data);
// 更新密码
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password, {
headers: {
'Content-Type': 'text/plain'
}
});
// 修改用户状态
export const changeUserStatus_api = (data: object) => server.patch(`/user`,data);
// 删除用户
export const deleteUser_api = (id: string) => server.remove(`/user/${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

@ -45,11 +45,9 @@
<div class="card-item-content">
<h3
@click="handlEye(slotProps.id)"
class="card-item-content-title"
class="card-item-content-title card-item-content-title-a"
>
<a class="card-item-content-title-a">{{
slotProps.name
}}</a>
{{ slotProps.name }}
</h3>
<a-row class="card-item-content-box">
<a-col
@ -402,9 +400,9 @@ const handleSearch = (e: any) => {
min-height: 100px;
.card-item-content-title-a {
// color: #000 !important;
color: #1890ff !important;
font-weight: 700;
font-size: 18px;
font-size: 16px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //

View File

@ -31,7 +31,7 @@
</template>
<template #content>
<div class="card-item-content">
<h3 class="card-item-content-title-a">
<h3 class="card-item-content-title card-item-content-title-a">
{{ slotProps.name }}
</h3>
<a-row class="card-item-content-box">
@ -299,9 +299,9 @@ const handleSearch = (e: any) => {
min-height: 100px;
.card-item-content-title-a {
// color: #000 !important;
color: #000 !important;
font-weight: 700;
font-size: 18px;
font-size: 16px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
export const FormStates = {
name: '',
type: 'UDP',
shareCluster: true,
parserType: '',
configuration: {
port: '',
host: '0.0.0.0',
publicPort: '',
publicHost: '',
remoteHost: '',
remotePort: '',
secure: false,
username: '',
password: '',
topicPrefix: '',
maxMessageSize: '',
certId: '',
privateKeyAlias: '',
clientId: '',
parserConfiguration: {
delimited: '',
lang: '',
script: '',
size: '',
length: '',
offset: '',
little: '',
},
},
description: '',
};
const VisibleMost = [
'COAP_SERVER',
'MQTT_SERVER',
'WEB_SOCKET_SERVER',
'TCP_SERVER',
'UDP',
'HTTP_SERVER',
];
export const VisibleData = {
parserType: ['TCP_SERVER'],
// configuration: {
port: VisibleMost,
host: VisibleMost,
publicPort: VisibleMost,
publicHost: VisibleMost,
remoteHost: ['MQTT_CLIENT'],
remotePort: ['MQTT_CLIENT'],
secure: ['TCP_SERVER', 'UDP', 'COAP_SERVER'],
username: ['MQTT_CLIENT'],
password: ['MQTT_CLIENT'],
topicPrefix: ['MQTT_CLIENT'],
maxMessageSize: ['MQTT_SERVER', 'MQTT_CLIENT'],
// certId: '',
// privateKeyAlias: '',
clientId: ['MQTT_CLIENT'],
// parserConfiguration: {
delimited: ['DELIMITED'],
lang: ['SCRIPT'],
script: ['SCRIPT'],
size: ['FIXED_LENGTH'],
length: ['LENGTH_FIELD'],
offset: ['LENGTH_FIELD'],
little: ['LENGTH_FIELD'],
// },
// },
};
export const ParserTypeOptions = [
{ value: 'DIRECT', label: '不处理' },
{ value: 'DELIMITED', label: '分隔符' },
{ value: 'SCRIPT', label: '自定义脚本' },
{ value: 'FIXED_LENGTH', label: '固定长度' },
{ value: 'LENGTH_FIELD', label: '长度字段' },
];
export const isVisible = (LastName: string, dependencies: string | boolean) =>
VisibleData[LastName].includes(dependencies);

View File

@ -0,0 +1,461 @@
<template>
<page-container>
<div>
<Search :columns="columns" target="search" @search="handleSearch" />
<JTable
ref="tableRef"
:columns="columns"
:gridColumn="3"
:request="query"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-button type="primary" @click="handlAdd"
><plus-outlined />新增</a-button
>
</template>
<template #card="slotProps">
<CardBox
:showStatus="true"
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:class="
slotProps.state.value === 'disabled'
? 'tableCardDisabled'
: 'tableCardEnabled'
"
:status="slotProps.state.value"
:statusText="slotProps.state.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/network.png')" />
</slot>
</template>
<template #content>
<div class="card-item-content">
<!-- <a
@click="handlEye(slotProps.id)"
class="card-item-content-title-a"
>
{{ slotProps.name }}
</a> -->
<h3
@click="handlEye(slotProps.id)"
class="card-item-content-title card-item-content-title-a"
>
{{ slotProps.name }}
</h3>
<a-row class="card-item-content-box">
<a-col :span="12">
<div class="card-item-content-text">
类型
</div>
<div class="card-item-content-text">
<a-tooltip>
<template #title>{{
slotProps.type
}}</template>
{{ slotProps.type }}
</a-tooltip>
</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
详情
</div>
<div class="card-item-content-text">
<a-tooltip>
<template #title>{{
getDetails(slotProps)
}}</template>
<span class="details-text">{{
getDetails(slotProps)
}}</span>
</a-tooltip>
</div>
</a-col>
</a-row>
</div>
</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 #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>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
<template #shareCluster="slotProps">
{{
slotProps.shareCluster === true
? '共享配置'
: '独立配置'
}}
</template>
<template #type="slotProps">
{{ slotProps.typeObject.name }}
</template>
<template #details="slotProps">
{{ getDetails(slotProps) }}
</template>
</JTable>
</div>
</page-container>
</template>
<script lang="ts" setup name="TypePage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { supports, query, remove, start, shutdown } from '@/api/link/type';
import { message } from 'ant-design-vue';
const tableRef = ref<Record<string, any>>({});
const router = useRouter();
const params = ref<Record<string, any>>({});
const options = ref([]);
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
width: 250,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
ellipsis: true,
width: 150,
search: {
type: 'select',
options: options,
},
scopedSlots: true,
},
{
title: '集群',
dataIndex: 'shareCluster',
key: 'shareCluster',
width: 120,
ellipsis: true,
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '共享配置', value: true },
{ label: '独立配置', value: false },
],
},
},
{
title: '详情',
dataIndex: 'details',
key: 'details',
ellipsis: true,
scopedSlots: true,
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
width: 100,
ellipsis: true,
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '正常', value: 'enabled' },
{ label: '禁用', value: 'disabled' },
],
},
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const state = data.state.value;
const actions = [
{
key: 'eye',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: async () => {
handlEye(data.id);
},
},
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
handlEdit(data.id);
},
},
{
key: 'action',
text: state === 'enabled' ? '禁用' : '启用',
tooltip: {
title: state === 'enabled' ? '禁用' : '启用',
},
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
popConfirm: {
title: `确认${state === 'enabled' ? '禁用' : '启用'}?`,
onConfirm: async () => {
let res =
state === 'enabled'
? await shutdown(data.id)
: await start(data.id);
if (res.success) {
message.success('操作成功');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: state === 'enabled',
tooltip: {
title:
state === 'enabled' ? '请先禁用该组件,再删除。' : '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const res = await remove(data.id);
if (res.success) {
message.success('操作成功');
tableRef.value.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
return type === 'table'
? actions
: actions.filter((item) => item.key !== 'eye');
};
const handlAdd = () => {
router.push({
path: `/iot/link/type/detail/:id`,
query: { view: false },
});
};
const handlEye = (id: string) => {
router.push({
path: `/iot/link/type/detail/${id}`,
query: { view: true },
});
};
const handlEdit = (id: string) => {
router.push({
path: `/iot/link/type/detail/${id}`,
query: { view: false },
});
};
const handlDelete = async (id: string) => {
const res = await remove(id);
if (res.success) {
message.success('操作成功');
tableRef.value.reload();
}
};
const getDetails = (slotProps: Partial<Record<string, any>>) => {
const { typeObject, shareCluster, configuration, cluster } = slotProps;
const headers =
typeObject.name.replace(/[^a-zA-Z]/g, '').toLowerCase() + '://';
const content = !!shareCluster
? (configuration.publicHost || configuration.remoteHost) +
':' +
(configuration.publicPort || configuration.remotePort)
: (cluster[0].configuration.publicHost ||
cluster[0].configuration.remoteHost) +
':' +
(cluster[0].configuration.publicPort ||
cluster[0].configuration.remotePort);
return headers + content;
};
const getSupports = async () => {
const res = await supports();
options.value = res.result.map((item) => ({
value: item.id,
label: item.name,
}));
};
getSupports();
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
};
</script>
<style lang="less" scoped>
.tableCardDisabled {
width: 100%;
background: url('/images/access-config-diaabled.png') no-repeat;
background-size: 100% 100%;
}
.tableCardEnabled {
width: 100%;
background: url('/images/access-config-enabled.png') no-repeat;
background-size: 100% 100%;
}
.card-item-content {
min-height: 100px;
.card-item-content-title-a {
// color: #000 !important;
font-weight: 700;
font-size: 16px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
.card-item-content-box {
min-height: 50px;
}
.card-item-content-text {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
}
.details-text {
font-weight: 700;
font-size: 14px;
}
</style>

0
src/views/link/Type/type.d.ts vendored Normal file
View File

View File

@ -265,7 +265,7 @@
</page-container>
</template>
<script lang="ts" setup name="CertificateDetail">
<script lang="ts" setup name="StreamDetail">
import { message, Form } from 'ant-design-vue';
import { queryProviders, queryDetail, save, update } from '@/api/media/stream';
import type { FormInstance } from 'ant-design-vue';

View File

@ -45,12 +45,12 @@
</template>
<template #content>
<div class="card-item-content">
<a
<h3
@click="handlEye(slotProps.id)"
class="card-item-content-title-a"
class="card-item-content-title card-item-content-title-a"
>
{{ slotProps.name }}
</a>
</h3>
<a-row class="card-item-content-box">
<a-col
:span="8"
@ -149,7 +149,7 @@
</div>
</page-container>
</template>
<script lang="ts" setup name="AccessConfigPage">
<script lang="ts" setup name="StreamPage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { query, remove, disable, enalbe } from '@/api/media/stream';
@ -300,7 +300,7 @@ const handleSearch = (e: any) => {
.card-item-content-title-a {
// color: #000 !important;
font-weight: 700;
font-size: 18px;
font-size: 16px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //

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
}

View File

@ -28,7 +28,7 @@
<a-checkbox-group v-model:value="bulkList" :options="options" />
</div>
<Search :columns="query.columns" @search="query.search" />
<Search :columns="props.queryColumns" @search="query.search" />
<JTable
ref="tableRef"
@ -118,6 +118,7 @@ import { message } from 'ant-design-vue';
const emits = defineEmits(['confirm']);
const props = defineProps<{
queryColumns: any[];
parentId: string;
allPermission: dictType;
assetType: 'product' | 'device';
@ -139,7 +140,6 @@ const dialog = {
permission: item.selectPermissions,
}));
// console.log(params);
dialog.loading.value = true;
bindDeviceOrProductList_api(props.assetType, params)
.then(() => {
@ -334,6 +334,14 @@ const table: any = {
data.forEach((item) => {
item.permissionList = permissionObj[item.id];
item.selectPermissions = ['read'];
//
if(props.assetType === 'product') {
item.state = {
value: item.state === 1 ? 'online': item.state === 0 ? 'offline': '',
text: item.state === 1 ? '正常': item.state === 0 ? '禁用': ''
}
}
});
resolve({

View File

@ -117,8 +117,8 @@ const form = reactive({
form.loading = true;
const api = form.data.id ? updateDepartment_api : addDepartment_api;
api(form.data)
.then(() => {
emits('refresh');
.then((resp:any) => {
emits('refresh',resp.result.id);
dialog.changeVisible(false);
})
.finally(() => (form.loading = false));

View File

@ -46,7 +46,6 @@ const dialog = {
},
//
changeVisible: (ids: string[], permissionList: string[]) => {
console.log(ids, permissionList);
form.permission = [...permissionList];
form.assetIdList = ids;
options.value = setOptions(permissionList);

View File

@ -10,7 +10,7 @@
<search-outlined />
</template>
</a-input>
<a-button type="primary" @click="openDialog" class="add-btn">
<a-button type="primary" @click="openDialog()" class="add-btn">
新增
</a-button>
<a-tree
@ -21,7 +21,7 @@
>
<template #title="{ name, data }">
<span>{{ name }}</span>
<span class="func-btns">
<span class="func-btns" @click="(e) => e.stopPropagation()">
<a-tooltip>
<template #title>编辑</template>
<a-button style="padding: 0" type="link">
@ -65,7 +65,7 @@
<EditDepartmentDialog
:tree-data="sourceTree"
ref="editDialogRef"
@refresh="getTree"
@refresh="refresh"
/>
</div>
</template>
@ -84,19 +84,15 @@ import {
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
const save = useRoute().query.save;
const emits = defineEmits(['change']);
const searchValue = ref('');//
const searchValue = ref(''); //
const loading = ref<boolean>(false); //
const sourceTree = ref<any[]>([]); //
const treeMap = new Map(); // map
const treeData = ref<any[]>([]); //
const selectedKeys = ref<string[]>([]); //
getTree();
watch(selectedKeys, (n) => {
emits('change', n[0]);
});
function getTree() {
loading.value = true;
const params = {
@ -119,7 +115,7 @@ function getTree() {
.finally(() => {
loading.value = false;
});
};
}
const search = debounce(() => {
const key = searchValue.value;
const treeArray = new Map();
@ -167,14 +163,30 @@ function delDepartment(id: string) {
getTree();
});
}
function refresh(id: string) {
// @ts-ignore
window?.onSaveSuccess && window.onSaveSuccess(id);
window.close();
getTree();
}
//
const editDialogRef = ref(); //
const openDialog = (row: any = {}) => {
editDialogRef.value.openDialog(true, row);
};
init();
function init() {
getTree();
watch(selectedKeys, (n) => {
emits('change', n[0]);
});
if (save) {
nextTick(() => {
openDialog();
});
}
}
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,33 @@
<template>
<a-modal
v-model:visible="visible"
title="绑定"
width="520px"
@ok="handleOk"
class="edit-dialog-container"
cancelText="取消"
okText="确定"
>
是否继续分配产品下的具体设备
</a-modal>
</template>
<script setup lang="ts">
const emits = defineEmits(['confirm']);
const visible = ref<boolean>(false);
const handleOk = () => {
emits('confirm');
changeVisible();
};
//
const changeVisible = () => {
visible.value = !visible.value;
};
//
defineExpose({
openDialog: changeVisible,
});
</script>
<style scoped></style>

View File

@ -125,6 +125,7 @@
<div class="dialogs">
<AddDeviceOrProductDialog
ref="addDialogRef"
:query-columns="query.columns"
:parent-id="props.parentId"
:all-permission="table.permissionList.value"
asset-type="device"
@ -155,14 +156,17 @@ import {
getPermission_api,
getPermissionDict_api,
unBindDeviceOrProduct_api,
getDeviceProduct_api,
} from '@/api/system/department';
import { intersection } from 'lodash-es';
import { dictType } from '../typing.d.ts';
import { message } from 'ant-design-vue';
const emits = defineEmits(['update:bindBool']);
const props = defineProps<{
parentId: string;
bindBool: boolean;
}>();
const query = {
columns: [
@ -186,6 +190,41 @@ const query = {
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',
@ -279,26 +318,30 @@ const table = {
const { pageIndex, pageSize, total, data } =
resp.result as resultType;
const ids = data.map((item) => item.id);
getPermission_api('device',ids, parentId).then((perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] = item.grantedPermissions;
});
data.forEach(
(item) => (item.permission = permissionObj[item.id]),
);
getPermission_api('device', ids, parentId).then(
(perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] =
item.grantedPermissions;
});
data.forEach(
(item) =>
(item.permission = permissionObj[item.id]),
);
resolve({
code: 200,
result: {
data: data,
pageIndex,
pageSize,
total,
},
status: 200,
});
});
resolve({
code: 200,
result: {
data: data,
pageIndex,
pageSize,
total,
},
status: 200,
});
},
);
});
}),
//
@ -393,6 +436,10 @@ const addDialogRef = ref();
const editDialogRef = ref();
table.init();
nextTick(() => {
props.bindBool && table.clickAdd();
emits('update:bindBool', false);
});
</script>
<style lang="less" scoped>

View File

@ -7,13 +7,19 @@
<div class="right">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="product" tab="产品">
<Product :parentId="departmentId" />
<Product
:parentId="departmentId"
@open-device-bind="openDeviceBind"
/>
</a-tab-pane>
<a-tab-pane key="device" tab="设备">
<Device :parentId="departmentId" />
<Device
:parentId="departmentId"
v-model:bindBool="bindBool"
/>
</a-tab-pane>
<a-tab-pane key="user" tab="用户">
<User />
<User :parentId="departmentId" />
</a-tab-pane>
</a-tabs>
</div>
@ -30,6 +36,12 @@ import User from './user/index.vue';
const activeKey = ref<'product' | 'device' | 'user'>('product');
const departmentId = ref<string>('');
const bindBool = ref<boolean>(false);
const openDeviceBind = () => {
bindBool.value = true;
activeKey.value = 'device';
};
</script>
<style lang="less" scoped>

View File

@ -125,10 +125,11 @@
<div class="dialogs">
<AddDeviceOrProductDialog
ref="addDialogRef"
:query-columns="query.columns"
:parent-id="props.parentId"
:all-permission="table.permissionList.value"
asset-type="product"
@confirm="table.refresh"
@confirm="table.addConfirm"
/>
<EditPermissionDialog
ref="editDialogRef"
@ -137,6 +138,7 @@
asset-type="product"
@confirm="table.refresh"
/>
<NextDialog ref="nextDialogRef" @confirm="emits('openDeviceBind')" />
</div>
</div>
</template>
@ -149,6 +151,7 @@ import {
} from '@ant-design/icons-vue';
import AddDeviceOrProductDialog from '../components/AddDeviceOrProductDialog.vue';
import EditPermissionDialog from '../components/EditPermissionDialog.vue';
import NextDialog from '../components/NextDialog.vue';
import { getImage } from '@/utils/comm';
import {
getDeviceOrProductList_api,
@ -161,6 +164,7 @@ import { intersection } from 'lodash-es';
import { dictType } from '../typing.d.ts';
import { message } from 'ant-design-vue';
const emits = defineEmits(['openDeviceBind'])
const props = defineProps<{
parentId: string;
}>();
@ -196,16 +200,12 @@ const query = {
type: 'select',
options: [
{
label: '在线',
value: 'online',
},
{
label: '离线',
value: 'offline',
label: '正常',
value: 1,
},
{
label: '禁用',
value: 'notActive',
value: 0,
},
],
},
@ -279,26 +279,43 @@ const table = {
const { pageIndex, pageSize, total, data } =
resp.result as resultType;
const ids = data.map((item) => item.id);
getPermission_api('product', ids, parentId).then((perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] = item.grantedPermissions;
});
data.forEach(
(item) => (item.permission = permissionObj[item.id]),
);
getPermission_api('product', ids, parentId).then(
(perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] =
item.grantedPermissions;
});
data.forEach((item) => {
item.permission = permissionObj[item.id];
item.state = {
value:
item.state === 1
? 'online'
: item.state === 0
? 'offline'
: '',
text:
item.state === 1
? '正常'
: item.state === 0
? '禁用'
: '',
};
});
resolve({
code: 200,
result: {
data: data,
pageIndex,
pageSize,
total,
},
status: 200,
});
});
resolve({
code: 200,
result: {
data: data,
pageIndex,
pageSize,
total,
},
status: 200,
});
},
);
});
}),
//
@ -350,6 +367,7 @@ const table = {
},
clickEdit: (row?: any) => {
const ids = row ? [row.id] : [...table._selectedRowKeys.value];
if (row || table.selectedRows.length === 1) {
const permissionList =
row?.permission || table.selectedRows[0].permission;
@ -387,11 +405,15 @@ const table = {
tableRef.value.reload();
});
},
addConfirm: ()=>{
table.refresh()
nextDialogRef.value && nextDialogRef.value.openDialog()
}
};
const addDialogRef = ref();
const editDialogRef = ref();
const nextDialogRef = ref();
table.init();
</script>

View File

@ -0,0 +1,165 @@
<template>
<a-modal
class="add-bind-user-dialog-container"
title="绑定"
width="1440px"
@ok="dialog.handleOk"
centered
:confirmLoading="dialog.loading.value"
cancelText="取消"
okText="确定"
v-model:visible="dialog.visible.value"
>
<Search :columns="query.columns" @search="query.search" />
<div class="table">
<JTable
ref="tableRef"
:columns="table.columns"
:request="table.requestFun"
:params="query.params"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys,
onChange: table.onSelectChange,
}"
@cancelSelect="table.cancelSelect"
model="TABLE"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
/>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { bindUser_api, getBindUserList_api } from '@/api/system/department';
import { message } from 'ant-design-vue';
const emits = defineEmits(['confirm']);
const props = defineProps({
parentId: String,
});
//
const dialog = {
loading: ref<boolean>(false),
visible: ref<boolean>(false),
handleOk: () => {
if (table._selectedRowKeys.length && props.parentId) {
bindUser_api(props.parentId, table._selectedRowKeys).then(() => {
emits('confirm');
message.success('操作成功');
dialog.changeVisible();
});
} else {
dialog.changeVisible();
}
},
//
changeVisible: () => {
if (!dialog.visible.value) query.search({});
dialog.visible.value = !dialog.visible.value;
},
};
//
defineExpose({
openDialog: dialog.changeVisible,
});
const query = {
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
},
};
const table = reactive({
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
],
_selectedRowKeys: [] as string[],
requestFun: async (oParams: any) => {
table.cancelSelect();
if (props.parentId) {
const params = {
...oParams,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
...oParams.terms,
{
terms: [
{
column: 'id$in-dimension$org$not',
value: props.parentId,
},
],
},
],
};
const resp: any = await getBindUserList_api(params);
return {
code: resp.status,
result: resp.result,
status: resp.status,
};
} else {
return {
code: 200,
result: {
data: [],
pageIndex: 0,
pageSize: 0,
total: 0,
},
status: 200,
};
}
},
onSelectChange: (keys: string[]) => {
table._selectedRowKeys = keys;
},
cancelSelect: () => {
table._selectedRowKeys = [];
},
});
</script>
<style lang="less" scoped>
:deep(.add-bind-user-dialog-container) {
.table {
height: 600px;
overflow-y: auto;
}
}
</style>

View File

@ -1,13 +1,226 @@
<template>
<div>
用户
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="table.requestFun"
:params="query.params"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys,
onChange: table.onSelectChange,
}"
@cancelSelect="table.cancelSelect"
model="TABLE"
>
<template #headerTitle>
<a-button
type="primary"
@click="table.openDialog"
style="margin-right: 10px"
>
<AIcon type="PlusOutlined" />绑定用户
</a-button>
<a-popconfirm
title="是否解除绑定"
ok-text="确定"
cancel-text="取消"
@confirm="table.unBind()"
>
<a-button
><AIcon type="DisconnectOutlined" />批量解绑</a-button
>
</a-popconfirm>
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.status"
:text="slotProps.status ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-popconfirm
title="是否解除绑定"
ok-text="确定"
cancel-text="取消"
@confirm="table.unBind(slotProps)"
>
<a-button style="padding: 0" type="link">
<AIcon type="DisconnectOutlined" />
</a-button>
</a-popconfirm>
</a-space>
</template>
</JTable>
<div class="dialogs">
<AddBindUserDialog
ref="addDialogRef"
:parent-id="props.parentId"
@confirm="table.refresh"
/>
</div>
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" name="user">
import AddBindUserDialog from './components/addBindUserDialog.vue';
import { getBindUserList_api, unBindUser_api } from '@/api/system/department';
import { message } from 'ant-design-vue';
const addDialogRef = ref();
const props = defineProps<{
parentId: string;
}>();
const query = {
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
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;
},
};
//
const tableRef = ref<Record<string, any>>({}); //
const table = reactive({
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
],
_selectedRowKeys: [] as string[],
requestFun: async (oParams: any) => {
table.cancelSelect();
if (props.parentId) {
const params = {
...oParams,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
...oParams.terms,
{
terms: [
{
column: 'id$in-dimension$org',
value: props.parentId,
},
],
},
],
};
const resp: any = await getBindUserList_api(params);
return {
code: resp.status,
result: resp.result,
status: resp.status,
};
} else {
return {
code: 200,
result: {
data: [],
pageIndex: 0,
pageSize: 0,
total: 0,
},
status: 200,
};
}
},
unBind: (row?: any) => {
const ids = row ? [row.id] : table._selectedRowKeys;
if (ids.length < 1) return message.warning('请勾选需要解绑的数据');
unBindUser_api(props.parentId, ids).then(() => {
message.success('操作成功');
table.refresh();
});
},
//
openDialog: () => {
addDialogRef.value && addDialogRef.value.openDialog();
},
onSelectChange: (keys: string[]) => {
table._selectedRowKeys = keys;
},
cancelSelect: () => {
table._selectedRowKeys = [];
},
//
refresh: () => {
tableRef.value.reload();
},
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -45,6 +45,7 @@
import { FormInstance, message } from 'ant-design-vue';
import { saveRole_api } from '@/api/system/role';
const router = useRouter();
const route = useRoute();
//
const dialog = reactive({
visible: false,
@ -56,12 +57,17 @@ const dialog = reactive({
if (resp.status === 200) {
message.success('操作成功');
dialog.visible = false;
router.push(`/system/Role/detail/${resp.result.id}`);
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={}) => {
changeVisible: (status: boolean, defaultForm: object = {}) => {
dialog.visible = status;
form.data = { name: '', description: '', ...defaultForm };
},
@ -76,12 +82,10 @@ const form = reactive({
},
});
//
defineExpose({
openDialog: dialog.changeVisible
})
openDialog: dialog.changeVisible,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,425 @@
<template>
<a-modal
v-model:visible="dialog.visible"
:title="dialog.title"
width="675px"
@ok="dialog.handleOk"
class="edit-dialog-container"
:confirmLoading="dialog.loading"
cancelText="取消"
okText="确定"
>
<a-form ref="formRef" :model="form.data" layout="vertical">
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
<a-col :span="12">
<a-form-item
name="name"
label="姓名"
:rules="[
{ required: true, message: '请输入姓名' },
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="form.data.name"
placeholder="请输入姓名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
name="username"
label="用户名"
:rules="[
{ required: true },
{
validator: form.rules.checkUserName,
trigger: 'blur',
},
]"
>
<a-input
v-model:value="form.data.username"
placeholder="请输入用户名"
:disabled="dialog.type === 'edit'"
/>
</a-form-item>
</a-col>
</a-row>
<a-row v-if="form.IsShow('add', 'reset')">
<a-col :span="24">
<a-form-item
name="password"
label="密码"
:rules="[
{ required: true },
{
validator: form.rules.checkPassword,
trigger: 'blur',
},
]"
>
<a-input-password
v-model:value="form.data.password"
placeholder="请输入密码"
/>
<!-- <Progress
:percent="20"
:steps="5"
:strokeColor="{ from: '#ff5500', to: '#ff9300' }"
/> -->
</a-form-item>
</a-col>
</a-row>
<a-row v-if="form.IsShow('add', 'reset')">
<a-col :span="24">
<a-form-item
name="confirmPassword"
label="确认密码"
:rules="[
{ required: true, message: '请输入8~64位的密码' },
{
validator: form.rules.checkAgainPassword,
trigger: 'change',
},
]"
>
<a-input-password
v-model:value="form.data.confirmPassword"
placeholder="请再次输入密码"
:maxlength="64"
/>
<!-- <Progress
:percent="60"
:steps="5"
:strokeColor="{ from: '#ff5500', to: '#ff9300' }"
/> -->
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
<a-col :span="12">
<a-form-item name="roleIdList" label="角色" class="flex">
<a-select
v-model:value="form.data.roleIdList"
mode="multiple"
style="width: 100%"
placeholder="请选择角色"
:options="form.roleOptions"
></a-select>
<span
class="add-item"
@click="form.clickAddItem('roleIdList', 'Role')"
><AIcon type="PlusOutlined"
/></span>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item name="orgIdList" label="组织" class="flex">
<a-tree-select
v-model:value="form.data.orgIdList"
show-search
style="width: 100%"
placeholder="请选择组织"
multiple
:tree-data="form.departmentOptions"
:fieldNames="{ label: 'name', value: 'id' }"
>
<template #title="{ name }">
{{ name }}
</template>
</a-tree-select>
<span
class="add-item"
@click="
form.clickAddItem('orgIdList', 'Department')
"
><AIcon type="PlusOutlined"
/></span>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
<a-col :span="12">
<a-form-item
name="telephone"
label="手机号"
:rules="[
{
pattern: /^1[3456789]\d{9}$/,
message: '请输入正确的手机号',
},
]"
>
<a-input
v-model:value="form.data.telephone"
placeholder="请输入手机号"
:maxlength="64"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
name="email"
label="邮箱"
:rules="[
{
pattern:
/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
message: '请输入正确的邮箱',
},
]"
>
<a-input
v-model:value="form.data.email"
placeholder="请输入邮箱"
:maxlength="64"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { FormInstance, message, TreeProps } from 'ant-design-vue';
// import Progress from './Progress.vue';
import {
validateField_api,
getRoleList_api,
getDepartmentList_api,
addUser_api,
updateUser_api,
updatePassword_api,
getUser_api,
} from '@/api/system/user';
import { Rule } from 'ant-design-vue/es/form';
import { DefaultOptionType } from 'ant-design-vue/es/vc-tree-select/TreeSelect';
import { AxiosResponse } from 'axios';
const emits = defineEmits(['confirm']);
//
const dialog = reactive({
title: '',
visible: false,
type: '' as modalType,
loading: false,
handleOk: () => {
formRef.value?.validate().then(() => {
form.submit(() => {
dialog.changeVisible('', {} as any);
emits('confirm');
});
});
},
/**
* 设置表单类型
* @param type 弹窗类型
* @param defaultForm 表单回显对象
*/
changeVisible: (type: modalType, defaultForm: formType) => {
dialog.setTitle(type);
form.getUserInfo(defaultForm.id || '', type);
dialog.type = type;
dialog.visible = type !== '';
},
setTitle: (type: modalType) => {
if (type === 'add') dialog.title = '新增';
else if (type === 'edit') dialog.title = '编辑';
else if (type === 'reset') dialog.title = '重置密码';
else dialog.title = '';
},
});
//
defineExpose({
openDialog: dialog.changeVisible,
});
const formRef = ref<FormInstance>();
const form = reactive({
data: {} as formType,
rules: {
checkUserName: (_rule: Rule, value: string): Promise<any> =>
new Promise((resolve, reject) => {
console.log(_rule);
if (dialog.type === 'edit') return resolve('');
if (!value) return reject('请输入用户名');
else if (value.length > 64) return reject('最多可输入64个字符');
validateField_api('username', value).then((resp: any): any => {
resp.result.passed
? resolve('')
: reject(resp.result.reason);
});
}),
checkPassword: (_rule: Rule, value: string): Promise<any> =>
new Promise((resolve, reject) => {
if (!value) return reject('请输入8~64位的密码');
else if (value.length > 64) return reject('最多可输入64个字符');
else if (value.length < 8) return reject('密码不能少于8位');
validateField_api('password', value).then((resp: any) => {
resp.result.passed
? resolve('')
: reject(resp.result.reason);
});
}),
checkAgainPassword: (_rule: Rule, value: string): Promise<any> => {
if (!value) return Promise.reject('');
return value === form.data.password
? Promise.resolve()
: Promise.reject('两次密码输入不一致');
},
//
},
roleOptions: [] as optionType[],
departmentOptions: [] as DefaultOptionType[],
init: () => {
form.getDepartmentList();
form.getRoleList();
},
getUserInfo: (id: string, type: modalType) => {
if (type === 'add') form.data = {} as formType;
else if (type === 'reset') form.data = { id } as formType;
else if (type === 'edit') {
getUser_api(id).then((resp: any) => {
form.data = {
...(resp.result as formType),
orgIdList: resp.result.orgList.map(
(item: dictType) => item.id,
),
roleIdList: resp.result.roleList.map(
(item: dictType) => item.id,
),
};
nextTick(() => {
formRef.value?.clearValidate();
});
});
}
},
submit: (cb?: Function) => {
let api: axiosFunType;
let params = {};
switch (dialog.type) {
case 'add': {
api = addUser_api;
params = {
user: form.data,
orgIdList: form.data.orgIdList,
roleIdList: form.data.roleIdList,
};
break;
}
case 'edit': {
api = updateUser_api;
params = {
id: form.data.id,
user: form.data,
orgIdList: form.data.orgIdList,
roleIdList: form.data.roleIdList,
};
break;
}
case 'reset': {
api = updatePassword_api;
params = {
id: form.data.id,
password: form.data.password,
};
break;
}
default:
return;
}
console.log(params);
api(params).then(() => {
message.success('操作成功');
cb && cb();
});
},
getRoleList: () => {
getRoleList_api().then((resp: any) => {
form.roleOptions = resp.result.map((item: dictType) => ({
label: item.name,
value: item.id,
}));
});
},
getDepartmentList: () => {
getDepartmentList_api().then((resp: any) => {
form.departmentOptions = resp.result;
});
},
IsShow: (...typeList: modalType[]) => typeList.includes(dialog.type),
clickAddItem: (prop: 'roleIdList' | 'orgIdList', target: string) => {
const tab: any = window.open(`${origin}/#/system/${target}?save=true`);
tab.onSaveSuccess = (value: string) => {
form.data[prop] = [...(form.data[prop] || []), value];
if (prop === 'roleIdList') form.getRoleList();
else form.getDepartmentList();
};
},
});
form.init();
interface AxiosResponseRewrite<T = any[]> extends AxiosResponse<T, any> {
result: T;
success: boolean;
}
type axiosFunType = (data: any) => Promise<AxiosResponseRewrite<unknown>>;
type modalType = '' | 'add' | 'edit' | 'reset';
type formType = {
id?: string;
name: string;
username: string;
password: string;
confirmPassword: string;
roleIdList: string[];
orgIdList: string[];
telephone: string;
email: string;
};
type dictType = {
id: string;
name: string;
children?: dictType;
};
type optionType = {
value: string;
label: string;
};
</script>
<style lang="less" scoped>
.edit-dialog-container {
.ant-form-item {
&.flex {
:deep(.ant-form-item-control-input-content) {
display: flex;
.ant-select {
flex: 1;
}
.add-item {
width: 32px;
height: 32px;
border: 1px solid #1d39c4;
color: #1d39c4;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px;
cursor: pointer;
}
}
}
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="progress-container">
<div class="value" :style="valueStyle"></div>
<div
class="split"
v-for="leftValue in valueArr"
:style="{ left: leftValue + '%' }"
></div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
steps?: number;
strokeColor?:
| {
from: string;
to: string;
}
| string;
percent: number;
}>(),
{
steps: 1,
strokeColor: '#108ee9',
},
);
const valueStyle = computed(() => {
let background = '';
if (typeof props.strokeColor === 'string') {
background = props.strokeColor;
} else {
background = `-webkit-linear-gradient(
left,
${props.strokeColor.from},
${props.strokeColor.to}
)`;
}
return {
background,
'clip-path': `polygon(0px 0px, ${props.percent}% 0px, ${props.percent}% 100%, 0px 100%)`,
};
});
const valueArr = computed(() => {
const result = [];
for (let i = 1; i < props.steps; i++) result.push((100 / props.steps) * i);
return result;
});
</script>
<style lang="less" scoped>
.progress-container {
width: 100%;
position: relative;
background-color: #e0e0e0;
height: 8px;
margin: 3px 0;
.split {
position: absolute;
top: 0;
width: 1px;
height: 100%;
background-color: #fff;
}
.value {
transition: all 0.35s ease-in-out 0s;
height: 100%;
position: relative;
}
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="user-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getUserList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
>
<template #headerTitle>
<a-button
type="primary"
@click="table.openDialog('add')"
style="margin-right: 10px"
><AIcon type="PlusOutlined" />新增</a-button
>
</template>
<template #type="slotProps">
{{ slotProps.type.name }}
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.status"
:text="slotProps.status ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip>
<template #title>编辑</template>
<a-button
style="padding: 0"
type="link"
@click="table.openDialog('edit', slotProps)"
>
<AIcon type="EditOutlined" />
</a-button>
</a-tooltip>
<a-popconfirm
:title="`确定${slotProps.status ? '禁用' : '启用'}吗?`"
ok-text="确定"
cancel-text="取消"
@confirm="table.changeStatus(slotProps)"
>
<a-tooltip>
<template #title>{{
slotProps.status ? '禁用' : '启用'
}}</template>
<a-button style="padding: 0" type="link">
<stop-outlined v-if="slotProps.status" />
<play-circle-outlined v-else />
</a-button>
</a-tooltip>
</a-popconfirm>
<a-tooltip>
<template #title>重置密码</template>
<a-button
style="padding: 0"
type="link"
@click="table.openDialog('reset', slotProps)"
>
<AIcon type="icon-zhongzhimima" />
</a-button>
</a-tooltip>
<a-popconfirm
title="确认删除"
ok-text="确定"
cancel-text="取消"
@confirm="table.clickDel(slotProps)"
:disabled="slotProps.status"
>
<a-tooltip>
<template #title>{{
slotProps.status ? '请先禁用,再删除' : '删除'
}}</template>
<a-button
style="padding: 0"
type="link"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template>
</JTable>
<div class="dialogs">
<EditUserDialog ref="editDialogRef" @confirm="table.refresh" />
</div>
</div>
</template>
<script setup lang="ts" name="UserMange">
import EditUserDialog from './components/EditUserDialog.vue';
import {
getUserType_api,
getUserList_api,
changeUserStatus_api,
deleteUser_api,
} from '@/api/system/user';
import { StopOutlined, PlayCircleOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
const query = {
columns: [
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '用户类型',
dataIndex: 'type',
key: 'type',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
getUserType_api().then((resp: any) => {
resolve(
resp.result.map((item: dictType) => ({
label: item.name,
value: item.id,
})),
);
});
}),
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
ellipsis: true,
search: {
rename: 'status',
type: 'select',
options: [
{
label: '启用',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
},
{
title: '手机号',
dataIndex: 'telephone',
key: 'telephone',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
],
params: ref({}),
search: (params: object) => {
query.params.value = params;
},
};
const editDialogRef = ref(); //
const tableRef = ref<Record<string, any>>({}); //
const table = {
columns: [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
},
{
title: '用户类型',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
},
{
title: '手机号',
dataIndex: 'telephone',
key: 'telephone',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
],
//
openDialog: (type: modalType, row?: any) => {
editDialogRef.value.openDialog(type, row || {});
},
changeStatus: (row: any) => {
const params = {
id: row.id,
status: row.status === 0 ? 1 : 0,
};
changeUserStatus_api(params).then(() => {
message.success('操作成功');
table.refresh();
});
},
//
clickDel: (row: any) => {
deleteUser_api(row.id).then(() => {
message.success('操作成功');
table.refresh();
});
},
//
refresh: () => {
tableRef.value.reload();
},
};
type dictType = {
id: string;
name: string;
};
type modalType = '' | 'add' | 'edit' | 'reset';
</script>
<style lang="less" scoped>
.user-container {
padding: 24px;
}
</style>